From ab9e8629ccb3552f3c25af0445c25b8a7a45a4f4 Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Thu, 13 Nov 2025 11:35:37 +0300 Subject: [PATCH 1/8] feat: refine ton_proof and sign data --- advanced/wallets/react-wallet-v2/package.json | 1 + .../wallets/react-wallet-v2/pnpm-lock.yaml | 10 + .../wallets/react-wallet-v2/src/lib/TonLib.ts | 238 ++++++++++++++++-- .../react-wallet-v2/src/utils/AuthUtil.ts | 21 +- .../src/utils/TonRequestHandlerUtil.ts | 11 +- .../src/views/SessionProposalModal.tsx | 9 +- .../SessionSignTonPersonalMessageModal.tsx | 2 +- .../views/SessionSignTonTransactionModal.tsx | 2 +- 8 files changed, 264 insertions(+), 30 deletions(-) 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..050f0ed11 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -9,9 +9,13 @@ import { Message, address, beginCell, - storeMessage + storeMessage, + storeStateInit } 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' /** * Types @@ -49,10 +53,22 @@ 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') } @@ -110,22 +126,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): 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', + publicKey: this.getPublicKey(), + timestamp, + domain, payload } @@ -156,13 +349,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') } @@ -180,6 +376,14 @@ export namespace TonLib { { signature: string; publicKey: string } > + export type TonProof = RPCRequest< + { iat: string; domain: string; payload: string }, + { + signature: string + publicKey: string + } + > + export type SendMessage = RPCRequest< { valid_until?: number 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..277dbc992 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts @@ -1,14 +1,16 @@ 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 approveTonRequest( + requestEvent: RequestEventArgs, + session: SessionTypes.Struct +) { const { params, id } = requestEvent const { chainId, request } = params @@ -20,7 +22,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) 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..f9acaacae 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,10 +434,17 @@ 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) + console.log('PROPOSAL', proposal); await walletkit.approveSession({ id: proposal.id, namespaces, diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx index d93816f03..31413943d 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx @@ -33,7 +33,7 @@ export default function SessionTonSignDataModal() { 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..a5afcf87c 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx @@ -35,7 +35,7 @@ export default function SessionTonSendMessageModal() { try { if (requestEvent) { setIsLoadingApprove(true) - const response = await approveTonRequest(requestEvent) + const response = await approveTonRequest(requestEvent, requestSession) await walletkit.respondSessionRequest({ topic, response From 4507e9671476bee94965afc8fd015375005ba7ce Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Fri, 14 Nov 2025 17:42:22 +0300 Subject: [PATCH 2/8] feat: remove unnecessary method --- advanced/wallets/react-wallet-v2/src/lib/TonLib.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index 050f0ed11..2c11f7d44 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -73,16 +73,6 @@ export default class TonLib { 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 async sendMessage( params: TonLib.SendMessage['params'], chainId: string From 6221efb37d0960abf6c65b7ce49f9a4802b3fa4f Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Fri, 14 Nov 2025 17:43:44 +0300 Subject: [PATCH 3/8] feat: remove unnecessary types --- advanced/wallets/react-wallet-v2/src/lib/TonLib.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index 2c11f7d44..c2d65e7b0 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -361,11 +361,6 @@ 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 }, { From 1c9448ee605120f81d4444e3a904800be45df22a Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Wed, 19 Nov 2025 12:13:00 +0300 Subject: [PATCH 4/8] feat: add retry --- .../wallets/react-wallet-v2/src/lib/TonLib.ts | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index c2d65e7b0..69545724e 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -4,10 +4,8 @@ import { TonClient, internal, Address, - Transaction, Cell, Message, - address, beginCell, storeMessage, storeStateInit @@ -17,6 +15,24 @@ 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 */ @@ -79,7 +95,7 @@ export default class TonLib { ): Promise { const client = this.getTonClient(chainId) const walletContract = client.open(this.wallet) - const seqno = await walletContract.getSeqno() + const seqno = await retry(() => walletContract.getSeqno()) const messages = (params.messages || []).map(m => { const amountBigInt = typeof m.amount === 'string' ? BigInt(m.amount) : BigInt(m.amount) return internal({ @@ -95,7 +111,7 @@ export default class TonLib { messages }) - await walletContract.send(transfer) + await retry(() => walletContract.send(transfer)) // Build external-in message for the result const message: Message = { @@ -288,17 +304,15 @@ export default class TonLib { return Buffer.from(encoded) } - public async signData(params: TonLib.SignData['params'], domain: string): Promise { + public async signData( + params: TonLib.SignData['params'], + domain: string + ): Promise { const payload: TonLib.SignData['params'] = params const timestamp = Math.floor(Date.now() / 1000) - const dataToSign = this.getToSign( - params, - this.wallet.address, - domain, - timestamp - ) + const dataToSign = this.getToSign(params, this.wallet.address, domain, timestamp) const signature = sign(dataToSign, this.keypair.secretKey) const addressStr = await this.getAddressRaw() From fe6beb353f6d5d46eb64fdd25282f98085601417 Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Wed, 19 Nov 2025 12:32:16 +0300 Subject: [PATCH 5/8] feat: fix send tx --- advanced/wallets/react-wallet-v2/src/lib/TonLib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index 69545724e..135038c01 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -101,7 +101,7 @@ export default class TonLib { return internal({ to: Address.parse(m.address), value: amountBigInt, - body: m.payload ?? 'Test transfer from ton WalletConnect' + body: m.payload ? Cell.fromBase64(m.payload) : 'Test transfer from ton WalletConnect', }) }) From 81496f6d21fd934458da9866ae046c8a13deff8a Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Wed, 19 Nov 2025 12:40:02 +0300 Subject: [PATCH 6/8] feat: fix send tx --- advanced/wallets/react-wallet-v2/src/lib/TonLib.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index 135038c01..7d32aa226 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -8,7 +8,7 @@ import { Message, beginCell, storeMessage, - storeStateInit + storeStateInit, loadStateInit } from '@ton/ton' import { TON_MAINNET_CHAINS, TON_TEST_CHAINS } from '@/data/TonData' import { sha256 } from '@noble/hashes/sha2' @@ -102,6 +102,7 @@ export default class TonLib { to: Address.parse(m.address), value: amountBigInt, body: m.payload ? Cell.fromBase64(m.payload) : 'Test transfer from ton WalletConnect', + init: m.stateInit ? loadStateInit(Cell.fromBase64(m.stateInit).beginParse()) : undefined, }) }) From fccad76a1efa579cb85ef2f75ac133c888ff6ea8 Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Wed, 19 Nov 2025 14:48:03 +0300 Subject: [PATCH 7/8] feat: add validation --- .../wallets/react-wallet-v2/src/lib/TonLib.ts | 122 +++++++++++++++++- .../src/utils/TonRequestHandlerUtil.ts | 21 +++ .../SessionSignTonPersonalMessageModal.tsx | 26 +++- .../views/SessionSignTonTransactionModal.tsx | 26 +++- 4 files changed, 187 insertions(+), 8 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index 7d32aa226..0bc31e757 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -8,7 +8,8 @@ import { Message, beginCell, storeMessage, - storeStateInit, loadStateInit + storeStateInit, + loadStateInit } from '@ton/ton' import { TON_MAINNET_CHAINS, TON_TEST_CHAINS } from '@/data/TonData' import { sha256 } from '@noble/hashes/sha2' @@ -41,6 +42,12 @@ interface IInitArguments { seed?: string } +export class TonValidationError extends Error { + constructor(message: string) { + super(`TonValidationError: ${message}`) + } +} + /** * Library */ @@ -89,6 +96,108 @@ export default class TonLib { return this.keypair.secretKey.toString('hex') } + 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.') + } + // TODO: should include validation for sufficient amount + + 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 @@ -96,20 +205,25 @@ export default class TonLib { const client = this.getTonClient(chainId) const walletContract = client.open(this.wallet) const seqno = await retry(() => walletContract.getSeqno()) + + this.validateSendMessage(params) + const messages = (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 ? Cell.fromBase64(m.payload) : 'Test transfer from ton WalletConnect', - init: m.stateInit ? loadStateInit(Cell.fromBase64(m.stateInit).beginParse()) : undefined, + init: m.stateInit ? loadStateInit(Cell.fromBase64(m.stateInit).beginParse()) : undefined }) }) const transfer = walletContract.createTransfer({ seqno, secretKey: this.keypair.secretKey, - messages + messages, + timeout: params.valid_until, }) await retry(() => walletContract.send(transfer)) @@ -390,7 +504,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/TonRequestHandlerUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts index 277dbc992..7df65689b 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts @@ -7,6 +7,27 @@ import { TON_SIGNING_METHODS } from '@/data/TonData' type RequestEventArgs = Omit +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 diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx index 31413943d..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,6 +32,24 @@ 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 { diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx index a5afcf87c..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,6 +34,24 @@ 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 { From 1d79609fd8254e8030d5268b45f5fff969ed2c6f Mon Sep 17 00:00:00 2001 From: Alejandbel Date: Thu, 20 Nov 2025 09:35:30 +0300 Subject: [PATCH 8/8] fix: fix sign data --- advanced/wallets/react-wallet-v2/src/lib/TonLib.ts | 11 ++++++----- .../src/utils/TonRequestHandlerUtil.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index 0bc31e757..7fc4dc541 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -98,7 +98,7 @@ export default class TonLib { public validateSendMessage(params: unknown) { if (typeof params !== 'object' || params === null) { - throw new TonValidationError('Invalid params'); + throw new TonValidationError('Invalid params') } if ('from' in params) { @@ -121,7 +121,7 @@ export default class TonLib { throw new TonValidationError('Valid until must be a number.') } - if (params.valid_until < (Date.now() / 1000)) { + if (params.valid_until < Date.now() / 1000) { throw new TonValidationError('Message is expired.') } } @@ -223,7 +223,7 @@ export default class TonLib { seqno, secretKey: this.keypair.secretKey, messages, - timeout: params.valid_until, + timeout: params.valid_until }) await retry(() => walletContract.send(transfer)) @@ -421,7 +421,8 @@ export default class TonLib { public async signData( params: TonLib.SignData['params'], - domain: string + domain: string, + chainId: string ): Promise { const payload: TonLib.SignData['params'] = params @@ -438,7 +439,7 @@ export default class TonLib { publicKey: this.getPublicKey(), timestamp, domain, - payload + payload: { ...payload, network: chainId.split(":")[1] } } try { diff --git a/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts index 7df65689b..2f8b172a4 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts @@ -44,7 +44,7 @@ export async function approveTonRequest( try { const payload = Array.isArray(request.params) ? request.params[0] : request.params const domain = new URL(session.peer.metadata.url).hostname - const result = await wallet.signData(payload, domain) + const result = await wallet.signData(payload, domain, chainId) return formatJsonRpcResult(id, result) } catch (error: any) { console.error(error)