wallet_connect_recharge_helper.dart 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import 'dart:async';
  2. import 'package:reown_appkit/modal/i_appkit_modal_impl.dart';
  3. import 'package:reown_appkit/reown_appkit.dart';
  4. import 'evm_recharge.dart';
  5. import '../../data/models/asset/recharge_order.dart';
  6. /// 使用 Reown AppKit 连接钱包并向订单收款地址发起转账(与 Web `useWalletConnectDeposit` 对齐)。
  7. class WalletConnectRechargeHelper {
  8. WalletConnectRechargeHelper._();
  9. static const _erc20Abi = '''
  10. [{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"type":"function"}]
  11. ''';
  12. static BigInt _parseUnits(String amount, int decimals) {
  13. final s = amount.trim();
  14. if (s.isEmpty) {
  15. throw FormatException('empty amount');
  16. }
  17. var normalized = s.replaceAll(',', '');
  18. final neg = normalized.startsWith('-');
  19. if (neg) {
  20. normalized = normalized.substring(1);
  21. }
  22. final parts = normalized.split('.');
  23. final intPart = parts[0].isEmpty ? '0' : parts[0];
  24. var frac = parts.length > 1 ? parts[1] : '';
  25. if (frac.length > decimals) {
  26. frac = frac.substring(0, decimals);
  27. } else {
  28. frac = frac.padRight(decimals, '0');
  29. }
  30. final combined = intPart + frac;
  31. var bi = BigInt.parse(combined);
  32. if (neg) {
  33. bi = -bi;
  34. }
  35. return bi;
  36. }
  37. static Future<void> ensureConnected(IReownAppKitModal modal) async {
  38. if (modal.isConnected) {
  39. return;
  40. }
  41. final completer = Completer<void>();
  42. void onConnect(ModalConnect _) {
  43. if (!completer.isCompleted) {
  44. completer.complete();
  45. }
  46. }
  47. modal.onModalConnect.subscribe(onConnect);
  48. try {
  49. // 与「先 await openModalView 再 await connect」不同:用户关掉弹窗未连接时,
  50. // openModalView 会结束而 onConnect 永不触发,会导致外层 loading 卡死到超时。
  51. final modalClosed = modal.openModalView();
  52. await Future.any<void>([
  53. completer.future,
  54. modalClosed,
  55. ]).timeout(
  56. const Duration(minutes: 3),
  57. onTimeout: () {
  58. throw TimeoutException('WalletConnect 连接超时');
  59. },
  60. );
  61. if (!modal.isConnected) {
  62. throw StateError('WalletConnect 已取消或未连接');
  63. }
  64. } finally {
  65. modal.onModalConnect.unsubscribe(onConnect);
  66. }
  67. }
  68. static Future<void> ensureEvmChain(
  69. IReownAppKitModal modal,
  70. int chainId,
  71. ) async {
  72. final net = ReownAppKitModalNetworks.getNetworkInfo(
  73. 'eip155',
  74. chainIdToCaip2(chainId),
  75. );
  76. if (net == null) {
  77. throw StateError('不支持的链 chainId=$chainId');
  78. }
  79. await modal.selectChain(net, switchChain: true);
  80. }
  81. /// 返回交易 hash(0x…)。
  82. static Future<String> connectAndPay({
  83. required IReownAppKitModal appKitModal,
  84. required RechargeFlatNetworkOption network,
  85. required RechargeOrderDetail order,
  86. }) async {
  87. final chainId = resolveEvmChainId(network.protocol, network.networkName);
  88. if (chainId == null) {
  89. throw StateError('当前网络不支持 WalletConnect,请使用钱包手动转账');
  90. }
  91. await ensureConnected(appKitModal);
  92. await ensureEvmChain(appKitModal, chainId);
  93. final session = appKitModal.session;
  94. if (session == null) {
  95. throw StateError('会话无效');
  96. }
  97. final fromRaw = session.getAddress('eip155');
  98. if (fromRaw == null || fromRaw.isEmpty) {
  99. throw StateError('未获取到钱包地址');
  100. }
  101. final from = EthereumAddress.fromHex(fromRaw);
  102. final to = EthereumAddress.fromHex(order.rechargeAddress.trim());
  103. final caip2 = chainIdToCaip2(chainId);
  104. final topic = session.topic;
  105. final amountStr = order.amount.trim();
  106. if (network.hasTokenContract) {
  107. final token = EthereumAddress.fromHex(network.contractAddress.trim());
  108. final decimals = erc20DecimalsForChain(chainId);
  109. final amountWei = _parseUnits(amountStr, decimals);
  110. final abi = ContractAbi.fromJson(_erc20Abi, 'ERC20');
  111. final deployed = DeployedContract(abi, token);
  112. final hash = await appKitModal.requestWriteContract(
  113. topic: topic,
  114. chainId: caip2,
  115. deployedContract: deployed,
  116. functionName: 'transfer',
  117. transaction: Transaction(from: from),
  118. parameters: [to, amountWei],
  119. );
  120. return hash.toString();
  121. } else {
  122. final valueWei = _parseUnits(amountStr, 18);
  123. final trx = Transaction(
  124. from: from,
  125. to: to,
  126. value: EtherAmount.fromBigInt(EtherUnit.wei, valueWei),
  127. );
  128. final hash = await appKitModal.request(
  129. topic: topic,
  130. chainId: caip2,
  131. request: SessionRequestParams(
  132. method: MethodsConstants.ethSendTransaction,
  133. params: [trx.toJson()],
  134. ),
  135. );
  136. return hash.toString();
  137. }
  138. }
  139. }