wallet_connect_tron_recharge.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import 'dart:convert';
  2. import 'package:reown_appkit/modal/i_appkit_modal_impl.dart';
  3. import 'package:reown_appkit/reown_appkit.dart';
  4. import '../config/app_config.dart';
  5. import '../../data/models/asset/recharge_order.dart';
  6. import 'tron_fullnode_client.dart';
  7. import 'tron_recharge.dart';
  8. import 'wallet_connect_recharge_helper.dart';
  9. /// 使用 Reown AppKit 的 Tron 命名空间签名 + FullNode 广播(与 Web `useWalletConnectTronDeposit.ts` 对齐)。
  10. class WalletConnectTronRecharge {
  11. WalletConnectTronRecharge._();
  12. static Map<String, dynamic> _deepMap(Object raw) {
  13. return jsonDecode(jsonEncode(raw)) as Map<String, dynamic>;
  14. }
  15. static Map<String, dynamic> _prepareUnsigned(Map<String, dynamic> tx) {
  16. final o = _deepMap(tx);
  17. o.remove('signature');
  18. final rd = o['raw_data'];
  19. if (rd is Map) {
  20. final contracts = rd['contract'];
  21. if (contracts is List) {
  22. for (final c in contracts) {
  23. if (c is Map && !c.containsKey('Permission_id')) {
  24. c['Permission_id'] = 0;
  25. }
  26. }
  27. }
  28. }
  29. return o;
  30. }
  31. static Map<String, dynamic> _cloneWithoutTxId(Map<String, dynamic> tx) {
  32. final c = _deepMap(tx);
  33. c.remove('txID');
  34. return c;
  35. }
  36. /// triggersmartcontract 完整响应的多种形态(与官方示例及常见钱包兼容)。
  37. static List<Map<String, dynamic>> _triggerEnvelopesForWallet(
  38. Map<String, dynamic> full,
  39. ) {
  40. final out = <Map<String, dynamic>>[_deepMap(full)];
  41. final inner = full['transaction'];
  42. if (inner is Map) {
  43. final innerMap = Map<String, dynamic>.from(inner);
  44. final preparedInner = _prepareUnsigned(innerMap);
  45. final wrapped = _deepMap(full);
  46. wrapped['transaction'] = preparedInner;
  47. out.add(wrapped);
  48. final noId = _deepMap(full);
  49. noId['transaction'] = _cloneWithoutTxId(preparedInner);
  50. out.add(noId);
  51. }
  52. return out;
  53. }
  54. static String _formatSignFailure(Object e) {
  55. if (e is ReownSignError) {
  56. final d = e.data;
  57. final extra = (d != null && d.isNotEmpty) ? ' data=$d' : '';
  58. return 'ReownSignError(code=${e.code}, message=${e.message})$extra';
  59. }
  60. return e.toString();
  61. }
  62. static Map<String, dynamic> _normalizeSigned(dynamic raw) {
  63. dynamic cur = raw;
  64. if (cur is String) {
  65. try {
  66. cur = jsonDecode(cur) as Object?;
  67. } catch (_) {
  68. throw StateError('钱包返回的签名格式无效');
  69. }
  70. }
  71. if (cur is! Map) {
  72. throw StateError('钱包未返回签名数据');
  73. }
  74. final m = Map<String, dynamic>.from(cur);
  75. if (m['result'] != null && m['result'] is Map) {
  76. return _normalizeSigned(m['result']);
  77. }
  78. if (m['signature'] is List &&
  79. (m['signature'] as List).isNotEmpty &&
  80. m['raw_data'] is Map) {
  81. return m;
  82. }
  83. for (final k in ['transaction', 'signedTransaction', 'tx']) {
  84. final inner = m[k];
  85. if (inner is Map) {
  86. try {
  87. return _normalizeSigned(inner);
  88. } catch (_) {
  89. // 尝试下一包裹字段
  90. }
  91. }
  92. }
  93. throw StateError('钱包未返回有效签名(signature)');
  94. }
  95. static bool _tronSessionUsesSignV1(ReownAppKitModalSession? session) {
  96. final v = session?.sessionProperties['tron_method_version'];
  97. return v == 'v1';
  98. }
  99. static String _tronSessionAccountString(
  100. ReownAppKitModalSession session,
  101. String base58,
  102. ) {
  103. final raw = session.getAccounts(namespace: 'tron') ?? [];
  104. for (final a in raw) {
  105. if (parseTronCaip10Address(a) == base58) {
  106. return a;
  107. }
  108. }
  109. return tronMainnetCaip10Account(base58);
  110. }
  111. static Future<Map<String, dynamic>> _requestTronSignTransaction({
  112. required IReownAppKitModal modal,
  113. required String topic,
  114. required ReownAppKitModalSession session,
  115. required String fromBase58,
  116. required Map<String, dynamic> unsigned,
  117. Map<String, dynamic>? triggerSmartContractBody,
  118. }) async {
  119. final prepared = _prepareUnsigned(unsigned);
  120. final noTxId = _cloneWithoutTxId(prepared);
  121. final sessionAcct = _tronSessionAccountString(session, fromBase58);
  122. final useV1 = _tronSessionUsesSignV1(session);
  123. final rawVariants = <Map<String, dynamic>>[];
  124. // 顺序:优先 Reown 官方示例(完整 triggersmartcontract 响应 + Base58 地址),
  125. // 其次 CAIP-10 账户字符串;内层仅 tx 时先扁平 transaction 再嵌套;全部 Map 试完后再试 JSON-RPC 风格的 [params]。
  126. if (triggerSmartContractBody != null) {
  127. for (final env in _triggerEnvelopesForWallet(triggerSmartContractBody)) {
  128. rawVariants.add({'address': fromBase58, 'transaction': env});
  129. rawVariants.add({'address': sessionAcct, 'transaction': env});
  130. }
  131. }
  132. if (useV1) {
  133. rawVariants.add({'address': fromBase58, 'transaction': prepared});
  134. rawVariants.add({'address': sessionAcct, 'transaction': prepared});
  135. rawVariants.add({
  136. 'address': fromBase58,
  137. 'transaction': {'transaction': prepared},
  138. });
  139. rawVariants.add({'transaction': prepared});
  140. rawVariants.add({'address': fromBase58, 'transaction': noTxId});
  141. rawVariants.add({'address': sessionAcct, 'transaction': noTxId});
  142. rawVariants.add({'transaction': noTxId});
  143. } else {
  144. rawVariants.add({'address': fromBase58, 'transaction': prepared});
  145. rawVariants.add({'address': sessionAcct, 'transaction': prepared});
  146. rawVariants.add({
  147. 'address': fromBase58,
  148. 'transaction': {'transaction': prepared},
  149. });
  150. rawVariants.add({
  151. 'address': sessionAcct,
  152. 'transaction': {'transaction': prepared},
  153. });
  154. rawVariants.add({'transaction': prepared});
  155. rawVariants.add({'transaction': {'transaction': prepared}});
  156. rawVariants.add({'address': fromBase58, 'transaction': noTxId});
  157. rawVariants.add({'address': sessionAcct, 'transaction': noTxId});
  158. rawVariants.add({
  159. 'address': fromBase58,
  160. 'transaction': {'transaction': noTxId},
  161. });
  162. rawVariants.add({
  163. 'address': sessionAcct,
  164. 'transaction': {'transaction': noTxId},
  165. });
  166. rawVariants.add({'transaction': noTxId});
  167. rawVariants.add({'transaction': {'transaction': noTxId}});
  168. }
  169. final variants = <dynamic>[
  170. ...rawVariants,
  171. ...rawVariants.map((m) => [m]),
  172. ];
  173. Object? lastErr;
  174. for (var i = 0; i < variants.length; i++) {
  175. try {
  176. final out = await modal.request(
  177. topic: topic,
  178. chainId: kTronMainnetCaip2,
  179. request: SessionRequestParams(
  180. method: 'tron_signTransaction',
  181. params: variants[i],
  182. ),
  183. );
  184. return _normalizeSigned(out);
  185. } catch (e) {
  186. lastErr = e;
  187. }
  188. }
  189. final err = lastErr;
  190. if (err == null) {
  191. throw StateError('tron_signTransaction 失败:未知错误');
  192. }
  193. throw StateError(
  194. 'tron_signTransaction 失败(已尝试多种参数格式)。最后错误:${_formatSignFailure(err)}',
  195. );
  196. }
  197. /// 检查当前会话是否含 `tron_signTransaction`(不抛异常,仅返回布尔值)。
  198. static bool _sessionHasTronSign(ReownAppKitModalSession? session) {
  199. final ns = session?.namespaces;
  200. if (ns == null || ns.isEmpty) {
  201. return false;
  202. }
  203. final methods = NamespaceUtils.getNamespacesMethodsForChainId(
  204. chainId: kTronMainnetCaip2,
  205. namespaces: ns,
  206. );
  207. return methods.contains('tron_signTransaction');
  208. }
  209. static Future<void> _ensureTronSession(IReownAppKitModal modal) async {
  210. await WalletConnectRechargeHelper.ensureConnected(modal);
  211. // 旧会话(在 optionalNamespaces 修复前握手建立)可能不含 tron 签名方法,需强制重连。
  212. var addr = modal.session?.getAddress('tron');
  213. final needsReconnect = addr == null ||
  214. addr.isEmpty ||
  215. !_sessionHasTronSign(modal.session);
  216. if (needsReconnect) {
  217. await modal.disconnect(disconnectAllSessions: true);
  218. await WalletConnectRechargeHelper.ensureConnected(modal);
  219. addr = modal.session?.getAddress('tron');
  220. }
  221. if (addr == null || !addr.startsWith('T')) {
  222. throw StateError('请使用支持波场(Tron)的钱包连接后重试');
  223. }
  224. if (!_sessionHasTronSign(modal.session)) {
  225. throw StateError(
  226. '钱包未授权 tron_signTransaction,请在钱包端确认 Tron 命名空间后重试',
  227. );
  228. }
  229. }
  230. static Future<void> _ensureTronChain(IReownAppKitModal modal) async {
  231. final net =
  232. ReownAppKitModalNetworks.getNetworkInfo('tron', kTronMainnetCaip2);
  233. if (net == null) {
  234. throw StateError('App 未注册 Tron 网络');
  235. }
  236. await modal.selectChain(net, switchChain: true);
  237. }
  238. /// 返回 Tron 交易 txid(非 0x 以太坊 hash)。
  239. static Future<String> connectAndPayTron({
  240. required IReownAppKitModal appKitModal,
  241. required RechargeFlatNetworkOption network,
  242. required RechargeOrderDetail order,
  243. }) async {
  244. if (!order.isPendingPayment) {
  245. throw StateError('订单状态不可支付');
  246. }
  247. if (!isTronDepositNetwork(network.protocol, network.networkName)) {
  248. throw StateError('当前网络不支持波场 WalletConnect');
  249. }
  250. await _ensureTronSession(appKitModal);
  251. await _ensureTronChain(appKitModal);
  252. final session = appKitModal.session;
  253. if (session == null) {
  254. throw StateError('会话无效');
  255. }
  256. final topic = session.topic;
  257. if (topic == null || topic.isEmpty) {
  258. throw StateError('会话无效');
  259. }
  260. final from = session.getAddress('tron');
  261. if (from == null || !from.startsWith('T')) {
  262. throw StateError('未获取到波场钱包地址');
  263. }
  264. final to = order.rechargeAddress.trim();
  265. if (!to.startsWith('T')) {
  266. throw StateError('收款地址不是有效的波场地址');
  267. }
  268. final chainNet =
  269. ReownAppKitModalNetworks.getNetworkInfo('tron', kTronMainnetCaip2);
  270. if (chainNet == null) {
  271. throw StateError('Tron 网络未配置');
  272. }
  273. var rpc = chainNet.rpcUrl.trim();
  274. if (rpc.isEmpty) {
  275. rpc = AppConfig.tronFullHost;
  276. }
  277. final amountStr = order.amount.trim();
  278. var contract = network.contractAddress.trim();
  279. if (contract.isEmpty &&
  280. (isTronRechargeUsdtSymbol(network.coinName) ||
  281. isTronRechargeUsdtSymbol(order.coinName))) {
  282. contract = kTrc20UsdtOfficialMainnet;
  283. }
  284. final Map<String, dynamic> unsigned;
  285. final Map<String, dynamic>? triggerBody;
  286. if (contract.isNotEmpty) {
  287. final minUnits = BigInt.parse(
  288. trc20AmountToMinUnits(amountStr, kTrc20UsdtDecimals),
  289. );
  290. final paramHex = tronTransferTrc20ParameterHex(to, minUnits);
  291. triggerBody = await TronFullNodeClient.triggerSmartContract(
  292. rpcBase: rpc,
  293. ownerAddressBase58: from,
  294. contractAddressBase58: contract,
  295. functionSelector: 'transfer(address,uint256)',
  296. parameterHex: paramHex,
  297. );
  298. final inner = triggerBody['transaction'];
  299. if (inner is! Map) {
  300. throw StateError('TRC20 构造响应缺少 transaction');
  301. }
  302. unsigned = Map<String, dynamic>.from(inner);
  303. } else {
  304. triggerBody = null;
  305. final sun = trxToSunAmount(amountStr);
  306. unsigned = await TronFullNodeClient.createTrxTransaction(
  307. rpcBase: rpc,
  308. ownerAddressBase58: from,
  309. toAddressBase58: to,
  310. amountSun: sun,
  311. );
  312. }
  313. final signed = await _requestTronSignTransaction(
  314. modal: appKitModal,
  315. topic: topic,
  316. session: session,
  317. fromBase58: from,
  318. unsigned: unsigned,
  319. triggerSmartContractBody: triggerBody,
  320. );
  321. return TronFullNodeClient.broadcastTransaction(
  322. rpcBase: rpc,
  323. signed: signed,
  324. );
  325. }
  326. }