import { WalletConnectClient } from './walletconnect-client' import { ConnectionService } from './connection-service' import { findAccountInNamespaces, parseAccount } from '../utils/account-utils' import { base58Encode } from '../utils/encoding-utils' import { parseError } from '../utils/error-utils' import { triggerSmartContract, stringToHex } from '../utils/tron-utils' import type { SignatureResult, EIP712TypedData } from '@meshconnect/uwc-types' import { toPersonalSignData, withEIP712Domain } from '@meshconnect/uwc-types' /** * Service for handling message signing in WalletConnect */ export class SignatureService { constructor( private walletConnectClient: WalletConnectClient, private connectionService: ConnectionService ) {} /** * Sign a message with the connected wallet */ async signMessage(message: string): Promise { try { const session = this.connectionService.getSession() if (!session) { throw new Error('No active session. Please connect first.') } const activeNetwork = this.connectionService.getActiveNetwork() const activeAddress = this.connectionService.getActiveAddress() if (!activeNetwork) { throw new Error('No active network. Please connect first.') } if (!activeAddress) { throw new Error('No active address. Please connect first.') } const client = await this.walletConnectClient.getClient() // Find the account that matches the active network and address const targetAccount = findAccountInNamespaces( session.namespaces, activeNetwork.id, activeAddress ) if (!targetAccount) { throw new Error( `No account found for network ${activeNetwork.id} and address ${activeAddress}` ) } const { namespace: accountNamespace, chainId, address } = parseAccount(targetAccount) // Determine the signing method based on namespace let method: string let params: unknown[] | unknown switch (accountNamespace) { case 'eip155': method = 'personal_sign' params = [toPersonalSignData(message), address] break case 'solana': { method = 'solana_signMessage' // For Solana: message needs to be base58 encoded const base58Message = base58Encode(message) // For Solana, params is an object, not an array params = { message: base58Message, pubkey: address } break } case 'tron': { // For TRON the user signs a mock transaction, since trust wallet returns incorrect TRON signatures const mockContract = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' const mockTransaction = await triggerSmartContract({ contractAddress: mockContract, functionSelector: 'approve(address,uint256)', feeLimit: 200000000, parameters: [ { type: 'address', value: address }, { type: 'uint256', value: 0 } ], ownerAddress: address }) if (!mockTransaction.transaction) { throw new Error('Failed to create mock transaction') } // Embed the message in the transaction data field mockTransaction.transaction.raw_data.data = stringToHex(message) method = 'tron_signTransaction' params = { address, transaction: { ...mockTransaction } } break } default: throw new Error( `Signing not supported for namespace: ${accountNamespace}` ) } // Make the signing request const result = await client.request({ topic: session.topic, chainId: `${accountNamespace}:${chainId}`, request: { method, params } }) // Handle different response formats. `address` is the exact signer // (parsed from the matched session account) — thread it through so // consumers claim it rather than a possibly-stale session address. if (accountNamespace === 'solana') { // Solana returns an object with a signature property return { signature: { type: 'standard', signature: (result as { signature: string }).signature }, address } } else if (accountNamespace === 'tron') { const tronResult = ( result as { result: { signature: string[]; txID: string } } ).result return { signature: { type: 'tron', signature: tronResult.signature, txID: tronResult.txID }, address } } return { signature: { type: 'standard', signature: result as string }, address } } catch (error) { throw parseError(error) } } /** * Sign EIP-712 typed structured data (eth_signTypedData_v4) over WalletConnect. * EVM-only (eip155); used by ERC-3009 and EIP-2612 relay flows. The WC session * already negotiates `eth_signTypedData_v4` in its namespace methods * (connection-service.ts), so no session renegotiation is needed. */ async signTypedData(typedData: EIP712TypedData): Promise { try { const session = this.connectionService.getSession() if (!session) { throw new Error('No active session. Please connect first.') } const activeNetwork = this.connectionService.getActiveNetwork() const activeAddress = this.connectionService.getActiveAddress() if (!activeNetwork) { throw new Error('No active network. Please connect first.') } if (!activeAddress) { throw new Error('No active address. Please connect first.') } const namespace = activeNetwork.id.split(':')[0] if (namespace !== 'eip155') { throw new Error( `signTypedData is only supported for EVM wallets, got: ${namespace}` ) } const client = await this.walletConnectClient.getClient() const chainId = activeNetwork.id.split(':')[1] // Inject EIP712Domain into the types before signing — mobile wallets // (Trust Wallet, OKX mobile, etc.) require it explicitly in the payload. const wireData = withEIP712Domain(typedData) const result = await client.request({ topic: session.topic, chainId: `eip155:${chainId}`, request: { method: 'eth_signTypedData_v4', params: [activeAddress, JSON.stringify(wireData)] } }) return result as string } catch (error) { throw parseError(error) } } }