huangjunling 1 week ago
parent
commit
c2bdf34094
33 changed files with 3117 additions and 0 deletions
  1. 9 0
      blockchain-transfer/.gitignore
  2. 36 0
      blockchain-transfer/README.en.md
  3. 37 0
      blockchain-transfer/README.md
  4. 211 0
      blockchain-transfer/pom.xml
  5. 12 0
      blockchain-transfer/src/main/java/com/table/transfer/TableTransferApplication.java
  6. 114 0
      blockchain-transfer/src/main/java/com/table/transfer/config/RabbitConfig.java
  7. 17 0
      blockchain-transfer/src/main/java/com/table/transfer/config/WebConfig.java
  8. 83 0
      blockchain-transfer/src/main/java/com/table/transfer/module/controller/TransferController.java
  9. 12 0
      blockchain-transfer/src/main/java/com/table/transfer/module/controller/TransferRecordController.java
  10. 24 0
      blockchain-transfer/src/main/java/com/table/transfer/module/enpty/ChainCoinDetail.java
  11. 138 0
      blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransactionInfoDTO.java
  12. 52 0
      blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransferRecord.java
  13. 49 0
      blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransferRequest.java
  14. 33 0
      blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransferResponse.java
  15. 56 0
      blockchain-transfer/src/main/java/com/table/transfer/module/enpty/Trc20TransferInfo.java
  16. 10 0
      blockchain-transfer/src/main/java/com/table/transfer/module/mapper/TransferRecordMapper.java
  17. 25 0
      blockchain-transfer/src/main/java/com/table/transfer/module/service/BlockchainService.java
  18. 523 0
      blockchain-transfer/src/main/java/com/table/transfer/module/service/EthereumService.java
  19. 12 0
      blockchain-transfer/src/main/java/com/table/transfer/module/service/Impl/TransferRecordServiceImpl.java
  20. 8 0
      blockchain-transfer/src/main/java/com/table/transfer/module/service/TransferRecordService.java
  21. 485 0
      blockchain-transfer/src/main/java/com/table/transfer/module/service/TronService.java
  22. 162 0
      blockchain-transfer/src/main/java/com/table/transfer/util/CryptoUtil.java
  23. 431 0
      blockchain-transfer/src/main/java/com/table/transfer/util/TronGridQueryUtil.java
  24. 18 0
      blockchain-transfer/src/main/java/com/table/transfer/util/aUtil.java
  25. 190 0
      blockchain-transfer/src/main/java/com/table/transfer/util/encryp/RSAEncryptionUtil.java
  26. 44 0
      blockchain-transfer/src/main/java/com/table/transfer/util/ether/ChainCoinDetailManager.java
  27. 47 0
      blockchain-transfer/src/main/java/com/table/transfer/util/ether/Web3jManager.java
  28. 90 0
      blockchain-transfer/src/main/java/com/table/transfer/util/tron/TronHexAndBase58Util.java
  29. 41 0
      blockchain-transfer/src/main/resources/application-dev.yaml
  30. 48 0
      blockchain-transfer/src/main/resources/application-local.yaml
  31. 41 0
      blockchain-transfer/src/main/resources/application-prod.yaml
  32. 4 0
      blockchain-transfer/src/main/resources/application.yaml
  33. 55 0
      blockchain-transfer/src/test/java/BlockchainServiceTest.java

+ 9 - 0
blockchain-transfer/.gitignore

@@ -0,0 +1,9 @@
+.idea
+target
+*.iml
+/*.iml
+/.idea/misc.xml
+/.idea/workspace.xml
+./table-admin/src/main/resources/application-dev.yml
+./table-api/src/main/resources/application-dev.yml
+*.log

+ 36 - 0
blockchain-transfer/README.en.md

@@ -0,0 +1,36 @@
+# blockchain-transfer
+
+#### Description
+区块链转账
+
+#### Software Architecture
+Software architecture description
+
+#### Installation
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Instructions
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Contribution
+
+1.  Fork the repository
+2.  Create Feat_xxx branch
+3.  Commit your code
+4.  Create Pull Request
+
+
+#### Gitee Feature
+
+1.  You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
+2.  Gitee blog [blog.gitee.com](https://blog.gitee.com)
+3.  Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
+4.  The most valuable open source project [GVP](https://gitee.com/gvp)
+5.  The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
+6.  The most popular members  [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 37 - 0
blockchain-transfer/README.md

@@ -0,0 +1,37 @@
+# blockchain-transfer
+
+#### 介绍
+区块链转账
+
+#### 软件架构
+软件架构说明
+
+
+#### 安装教程
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### 使用说明
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### 参与贡献
+
+1.  Fork 本仓库
+2.  新建 Feat_xxx 分支
+3.  提交代码
+4.  新建 Pull Request
+
+
+#### 特技
+
+1.  使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
+2.  Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
+3.  你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
+4.  [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
+5.  Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
+6.  Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 211 - 0
blockchain-transfer/pom.xml

@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+         http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.1.5</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>blockchain-transfer</artifactId>
+    <groupId>com.table</groupId>
+
+    <packaging>jar</packaging>
+
+    <name>Blockchain Transfer Service</name>
+    <description>ETH/BNB/TRON transfer service</description>
+
+    <properties>
+        <java.version>20</java.version>
+        <maven.compiler.source>20</maven.compiler.source>
+        <maven.compiler.target>20</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!-- Spring Boot Starter Web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- Spring Boot Starter Validation -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- HTTP Client -->
+        <dependency>
+            <groupId>org.apache.httpcomponents.client5</groupId>
+            <artifactId>httpclient5</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>3.5.4</version>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>8.0.33</version>
+        </dependency>
+        <!-- JSON Processing -->
+
+        <!-- Ethereum cryptography -->
+        <dependency>
+            <groupId>org.web3j</groupId>
+            <artifactId>core</artifactId>
+            <version>4.9.8</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+                <!-- 排除旧版本的 OkHttp -->
+                <exclusion>
+                    <groupId>com.squareup.okhttp3</groupId>
+                    <artifactId>okhttp</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.squareup.okio</groupId>
+                    <artifactId>okio</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- 统一引入较新版本的 OkHttp(无 Kotlin 依赖的版本) -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.12.0</version>
+        </dependency>
+        <dependency>
+            <groupId>io.github.tronprotocol</groupId>
+            <artifactId>trident</artifactId>
+            <version>0.10.0</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-http</artifactId>
+            <version>5.8.39</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-json -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-json</artifactId>
+            <version>5.8.39</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-crypto</artifactId>
+            <version>5.8.39</version>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-amqp</artifactId>
+        </dependency>
+    </dependencies>
+    <!--打包配置-->
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+                <includes>
+                    <include>application.yaml</include>
+                </includes>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>false</filtering>
+                <includes>
+                    <!-- 只包含当前激活环境的配置文件 -->
+                    <include>application-${profile.active}.yaml</include>
+                    <include>application-${profile.active}.properties</include>
+                </includes>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>false</filtering>
+                <excludes>
+                    <!-- 排除所有其他环境的配置文件 -->
+                    <exclude>application-*.yaml</exclude>
+                    <exclude>application-*.properties</exclude>
+                    <exclude>bootstrap-*.yaml</exclude>
+                    <exclude>bootstrap-*.properties</exclude>
+                </excludes>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+
+            <!-- 资源过滤插件 -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <version>3.3.1</version>
+                <configuration>
+                    <propertiesEncoding>UTF-8</propertiesEncoding>
+                    <useDefaultDelimiters>true</useDefaultDelimiters>
+                    <delimiters>
+                        <delimiter>@</delimiter>
+                    </delimiters>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>dev</id>
+            <properties>
+                <profile.active>dev</profile.active>
+            </properties>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+
+        <profile>
+            <id>local</id>
+            <properties>
+                <profile.active>local</profile.active>
+            </properties>
+        </profile>
+
+        <profile>
+            <id>prod</id>
+            <properties>
+                <profile.active>prod</profile.active>
+            </properties>
+        </profile>
+    </profiles>
+    
+</project>

+ 12 - 0
blockchain-transfer/src/main/java/com/table/transfer/TableTransferApplication.java

@@ -0,0 +1,12 @@
+package com.table.transfer;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+@SpringBootApplication
+public class TableTransferApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(TableTransferApplication.class, args);
+    }
+}

+ 114 - 0
blockchain-transfer/src/main/java/com/table/transfer/config/RabbitConfig.java

@@ -0,0 +1,114 @@
+package com.table.transfer.config;
+
+import org.springframework.amqp.core.*;
+import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
+import org.springframework.amqp.rabbit.connection.ConnectionFactory;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+
+// RabbitConfig.java
+@Component
+public class RabbitConfig {
+    public static final String TRANSFER_EXCHANGE = "transfer";
+
+    // 请求队列和路由键
+    public static final String TRANSFER_REQUEST_QUEUE = "transfer.request";
+    public static final String TRANSFER_ETH = "ETH";
+    public static final String TRANSFER_BSC = "BSC";
+    public static final String TRANSFER_TRON = "TRON";
+
+    // 结果队列和路由键
+    public static final String TRANSFER_RESULT_QUEUE = "transfer.result";
+    public static final String TRANSFER_RESULT = "result";
+
+    // 请求队列
+    @Bean
+    public Queue transferRequestQueue() {
+        return QueueBuilder.durable(TRANSFER_REQUEST_QUEUE)
+                .singleActiveConsumer()
+                .build();
+    }
+
+    // 结果队列
+    @Bean
+    public Queue transferResultQueue() {
+        return QueueBuilder.durable(TRANSFER_RESULT_QUEUE)
+                .singleActiveConsumer()
+                .build();
+    }
+
+    // 只需要一个交换机
+    @Bean
+    public DirectExchange transferExchange() {
+        return new DirectExchange(TRANSFER_EXCHANGE);
+    }
+
+    // 请求队列的绑定(修正路由键)
+    @Bean
+    public Binding transferETHBinding() {
+        return BindingBuilder.bind(transferRequestQueue())
+                .to(transferExchange())
+                .with(TRANSFER_ETH);
+    }
+
+    @Bean
+    public Binding transferBSCBinding() {
+        return BindingBuilder.bind(transferRequestQueue())
+                .to(transferExchange())
+                .with(TRANSFER_BSC);
+    }
+
+    @Bean
+    public Binding transferTRONBinding() {
+        return BindingBuilder.bind(transferRequestQueue())
+                .to(transferExchange())
+                .with(TRANSFER_TRON);
+    }
+
+    // 结果队列的绑定
+    @Bean
+    public Binding transferResultBinding() {
+        return BindingBuilder.bind(transferResultQueue())
+                .to(transferExchange())
+                .with(TRANSFER_RESULT);
+    }
+
+    @Bean
+    public MessageConverter jsonMessageConverter() {
+        return new Jackson2JsonMessageConverter();
+    }
+
+    /**
+     * 配置RabbitTemplate使用JSON转换器
+     */
+    @Bean
+    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
+        RabbitTemplate template = new RabbitTemplate(connectionFactory);
+        template.setMessageConverter(jsonMessageConverter());
+        return template;
+    }
+
+    /**
+     * 配置监听容器工厂使用JSON转换器
+     */
+    @Bean
+    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
+            ConnectionFactory connectionFactory) {
+        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
+        factory.setConnectionFactory(connectionFactory);
+        factory.setMessageConverter(jsonMessageConverter());
+
+        // 可选:配置并发等参数
+        factory.setConcurrentConsumers(1);
+        factory.setMaxConcurrentConsumers(1);
+        factory.setPrefetchCount(1); // 串行处理,每次只处理一个消息
+
+        return factory;
+    }
+
+}

+ 17 - 0
blockchain-transfer/src/main/java/com/table/transfer/config/WebConfig.java

@@ -0,0 +1,17 @@
+package com.table.transfer.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+    
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOrigins("http://localhost", "http://127.0.0.1")
+                .allowedMethods("POST")
+                .allowedHeaders("*");
+    }
+}

+ 83 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/controller/TransferController.java

@@ -0,0 +1,83 @@
+package com.table.transfer.module.controller;
+
+
+import com.rabbitmq.client.Channel;
+import com.table.transfer.config.RabbitConfig;
+import com.table.transfer.module.enpty.TransferRecord;
+import com.table.transfer.module.enpty.TransferRequest;
+import com.table.transfer.module.enpty.TransferResponse;
+import com.table.transfer.module.service.BlockchainService;
+import com.table.transfer.module.service.EthereumService;
+import com.table.transfer.module.service.TransferRecordService;
+import com.table.transfer.module.service.TronService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.support.AmqpHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.io.IOException;
+
+
+@Slf4j
+@Validated
+@Component
+public class TransferController {
+
+    @Resource
+    TronService tronService;
+    @Resource
+    EthereumService ethereumService;
+    @Resource
+    RabbitTemplate rabbitTemplate;
+
+    @Resource
+    TransferRecordService transferRecordService;
+
+    @RabbitListener(queues = RabbitConfig.TRANSFER_REQUEST_QUEUE, ackMode = "MANUAL")
+    public void transfer (@RequestBody TransferRequest request, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
+        TransferResponse response = new TransferResponse();
+        try {
+            log.info("收到转账请求: chain={}, to={}, amount={}", request.getChain(), request.getToAddress(), request.getAmount());
+            String chain = request.getChain().toUpperCase();
+            if (chain.equals("TRON")){
+                response= tronService.transfer(request);
+            }
+            if (chain.equals("ETH")||chain.equals("BSC")){
+                if(chain.equals("ETH")){}
+                Thread.sleep(5000);
+                response= ethereumService.transfer(request);
+
+            }
+
+            if (response.isSuccess()) {
+                log.info("转账成功: txHash={}", response.getTransactionHash());
+            } else {
+                log.error("转账失败: error={}", response.getMessage());
+            }
+        } catch (Exception e) {
+            response = TransferResponse.error(e.getMessage());
+            log.error("转账处理异常: {}", e.getMessage(), e);
+        } finally {
+            response.setRequestId(request.getRequestId());
+            try {
+                TransferRecord record = TransferRecord.create(request, response);
+                transferRecordService.save(record);
+            }catch (Exception e){
+                log.error("什么?写入转账记录失败了?{}", e.getMessage(), e);
+            }
+
+            //回调发送结果
+            rabbitTemplate.convertAndSend(
+                    RabbitConfig.TRANSFER_EXCHANGE,
+                    RabbitConfig.TRANSFER_RESULT,
+                    response
+            );
+            channel.basicAck(deliveryTag, false); // 总是 ACK,绝不 requeue,避免重复转账
+        }
+    }
+}

+ 12 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/controller/TransferRecordController.java

@@ -0,0 +1,12 @@
+package com.table.transfer.module.controller;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("transfer")
+public class TransferRecordController {
+
+
+
+}

+ 24 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/enpty/ChainCoinDetail.java

@@ -0,0 +1,24 @@
+package com.table.transfer.module.enpty;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+@Data
+@AllArgsConstructor
+public class ChainCoinDetail {
+    private Integer chainId;
+    private String chain;             // 链标识:eth, bsc, polygon 等
+    private String coin;//币-链
+    private String contractAddress;   // 代币合约地址,主币为 null
+    private Integer decimal;          // 代币精度,主币默认 18
+    private BigInteger minGasPrice;   // 最小 gas price(Gwei)
+    private BigInteger maxGasPrice;   // 最大 gas price(Gwei)
+    private String nodeUrl;// 节点 RPC 地址
+
+    public String getAssetCode(){
+        return coin+"-"+chain;
+    }
+}

+ 138 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransactionInfoDTO.java

@@ -0,0 +1,138 @@
+package com.table.transfer.module.enpty;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 用于封装不同区块链(如 TRON、Ethereum、BNB Chain 等)的交易信息,
+ * 将各异的原始 API 返回结构标准化,便于业务层统一处理。
+ * 支持主币(TRX/ETH/BNB)和代币(USDT、USDC 等)转账的统一表示。
+ */
+@Data
+public final class TransactionInfoDTO {
+
+    /**
+     * 交易哈希(Hash)
+     * 区块链上交易的唯一标识符
+     */
+    private String hash;
+
+    /**
+     * 发送方地址
+     * 资产转出的源地址
+     */
+    private String fromAddress;
+
+    /**
+     * 接收方地址
+     * 资产转入的目标地址
+     */
+    private String toAddress;
+
+    /**
+     * 转账金额(主单位)
+     * 已根据代币精度转换为人类可读单位,例如:
+     * - 13.611877 USDT(非 13611877 Sun)
+     * - 0.5 TRX / ETH / BNB
+     */
+    private BigDecimal amount;
+
+    /**
+     * 代币符号
+     * 例如:TRX, ETH, BNB, USDT, USDC, BTC
+     */
+    private String symbol;
+
+    /**
+     * 代币类型
+     * 取值包括:
+     * - NATIVE:主币(如 TRX、ETH、BNB)
+     * - ERC20:Ethereum 上的代币
+     * - BEP20:BNB Chain 上的代币
+     * - TRC20:TRON 上的代币
+     * - SPL:Solana 代币(可扩展)
+     */
+    private String tokenType;
+
+    /**
+     * 代币合约地址
+     * - 对于代币转账:填写代币的智能合约地址(如 USDT 合约)
+     * - 对于主币转账:填写 null 或空字符串
+     */
+    private String contractAddress;
+
+    /**
+     * 交易时间戳(毫秒)
+     * Unix 时间戳,单位为毫秒(例如:1758089232000)
+     */
+    private Long timestamp;
+
+    /**
+     * 区块链名称
+     * 标识该交易所属的链,例如:
+     * - tron
+     * - ethereum
+     * - bnb
+     * - polygon
+     * - arbitrum
+     * - optimism
+     */
+    private String chain;
+
+    /**
+     * 交易状态
+     * 取值:
+     * - success:成功
+     * - failed:失败
+     * - pending:待确认
+     * - reverted:已回滚
+     */
+    private String status;
+
+
+    /**
+     * 所在区块高度
+     * 交易被打包的区块编号
+     */
+    private Long blockNumber;
+
+    /**
+     * 交易手续费(主单位)
+     * 已转换为链的主币单位,例如:
+     * - 0.0021 ETH
+     * - 0.1 TRX
+     * - 0.0003 BNB
+     */
+    private BigDecimal gasFee;
+
+
+    /**
+     * 代币精度(小数位数)
+     * 例如:
+     * - USDT/TRC20: 6
+     * - TRX: 6
+     * - BNB: 18
+     * 用于金额转换和展示
+     */
+    private Integer decimals;
+
+    /**
+     * 调用的方法名(Function Name)
+     * 例如:transfer, approve, swapExactTokensForTokens
+     * 主要用于合约交互类交易
+     */
+    private String methodName;
+
+    /**
+     * 交易类型
+     * 用于分类交易行为,例如:
+     * - transfer:转账
+     * - approve:授权
+     * - swap:兑换
+     * - mint:铸币
+     * - burn:销毁
+     */
+    private String type;
+
+}

+ 52 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransferRecord.java

@@ -0,0 +1,52 @@
+package com.table.transfer.module.enpty;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_transfer_records")
+public class TransferRecord {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    private String requestId;
+    
+    private String fromAddress;
+    
+    private String toAddress;
+    
+    private BigDecimal amount;
+
+    private String transactionHash;
+
+    private String status; // SUCCESS, FAILED, PENDING
+    
+    private String Message;
+    
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+    
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    private boolean deleted;
+    public static TransferRecord create(TransferRequest request,TransferResponse response){
+        TransferRecord record =new TransferRecord();
+        record.setRequestId(request.getRequestId());
+        record.setAmount(new BigDecimal(request.getAmount()));
+        record.setFromAddress(request.getFromAddress());
+        record.setToAddress(request.getToAddress());
+
+        LocalDateTime now = LocalDateTime.now();
+        record.setCreateTime(now);
+        record.setUpdateTime(now);
+
+        record.setStatus(response.isSuccess()?"SUCCESS":"FAILED");
+        record.setTransactionHash(response.getTransactionHash());
+        record.setMessage(response.getMessage());
+        return record;
+    };
+}

+ 49 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransferRequest.java

@@ -0,0 +1,49 @@
+package com.table.transfer.module.enpty;
+
+import cn.hutool.core.util.StrUtil;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+
+import java.math.BigInteger;
+
+
+@Data
+public class TransferRequest {
+    /**
+     * 请求方自己携带的用于区分的id
+     */
+    private String requestId;
+    @NotBlank(message = "链类型不能为空")
+    private String chain; // ETH, BSC, TRON
+    @NotBlank(message = "链类型不能为空")
+    private String coin; // ETH, BSC, TRON
+
+    private String privateKey;
+    @NotBlank(message = "加密私钥不能为空")
+    private String encryptionPrivateKey;
+    @NotBlank(message = "发送地址不能为空")
+    private String fromAddress;
+    @NotBlank(message = "接收地址不能为空")
+    private String toAddress;
+
+    @NotBlank(message = "转账金额不能为空")
+    @Pattern(regexp = "^[0-9]+(\\.[0-9]+)?$", message = "金额格式不正确")
+    private String amount;
+    private BigInteger amountWei;
+    private String contractAddress; // 代币合约地址,为空表示主币转账
+
+    @Pattern(regexp = "^[0-9]*$", message = "Gas Limit必须为数字")
+    private String gasLimit;
+
+    @Pattern(regexp = "^[0-9]+(\\.[0-9]+)?$", message = "Gas Price格式不正确")
+    private String gasPrice;
+
+    public String getAssetCode() {
+        return coin + "-" + chain;
+    }
+
+    public boolean isTokenTransfer() {
+       return !StrUtil.equalsAnyIgnoreCase(getAssetCode(), "ETH-ETH", "BNB-BSC", "TRX-TRON");
+    }
+}

+ 33 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/enpty/TransferResponse.java

@@ -0,0 +1,33 @@
+package com.table.transfer.module.enpty;
+
+import lombok.Data;
+
+@Data
+public class TransferResponse {
+    private boolean success;
+    private String message;
+    /**
+     * 请求方自己携带的用于区分的id
+     */
+    private String requestId;
+    /**
+     * 转账服务用来查询的id
+     */
+    private String responseId;
+    private String transactionHash;
+    
+    public static TransferResponse success(String transactionHash) {
+        TransferResponse response = new TransferResponse();
+        response.setSuccess(true);
+        response.setMessage("转账成功");
+        response.setTransactionHash(transactionHash);
+        return response;
+    }
+    
+    public static TransferResponse error(String error) {
+        TransferResponse response = new TransferResponse();
+        response.setSuccess(false);
+        response.setMessage(error);
+        return response;
+    }
+}

+ 56 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/enpty/Trc20TransferInfo.java

@@ -0,0 +1,56 @@
+package com.table.transfer.module.enpty;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+@Data
+public class Trc20TransferInfo {
+    private String icon_url;
+    private String symbol;
+    private String level;
+    private String to_address;
+    private String tokenType2;
+    private String contract_address;
+    private String type;
+    private int decimals;
+    private String name;
+    private boolean vip;
+    private String tokenType;
+    private String from_address;
+    private String amount_str;
+    private int status;
+
+    /**
+     * 【注意】此字段单位为 SUN
+     * 请优先使用 {@link #getDecimalsAmount()} 获取 代币 单位余额
+     * @deprecated 此方法返回的是未转换的余额,请确认需要的是sun单位
+     */
+    @Deprecated
+    public String getAmount_str() {
+        return amount_str;
+    }
+    public BigDecimal getDecimalsAmount() {
+        return new BigDecimal(amount_str).divide(
+                BigDecimal.valueOf(Math.pow(10, decimals)),
+                decimals,
+                RoundingMode.DOWN
+        );
+    }
+
+    public TransactionInfoDTO Trc20TransferInfo2TransactionInfoDTO(){
+        TransactionInfoDTO transactionInfoDTO = new TransactionInfoDTO();
+        transactionInfoDTO.setSymbol(symbol);
+        transactionInfoDTO.setAmount(getDecimalsAmount());
+        transactionInfoDTO.setTokenType(getTokenType());
+        transactionInfoDTO.setFromAddress(from_address);
+        transactionInfoDTO.setTokenType(tokenType2);
+        transactionInfoDTO.setContractAddress(contract_address);
+        transactionInfoDTO.setType(type);
+        transactionInfoDTO.setDecimals(decimals);
+        transactionInfoDTO.setToAddress(to_address);
+        return transactionInfoDTO;
+    }
+
+}

+ 10 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/mapper/TransferRecordMapper.java

@@ -0,0 +1,10 @@
+package com.table.transfer.module.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.table.transfer.module.enpty.TransferRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TransferRecordMapper extends BaseMapper<TransferRecord> {
+
+}

+ 25 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/service/BlockchainService.java

@@ -0,0 +1,25 @@
+package com.table.transfer.module.service;
+
+
+import com.table.transfer.module.enpty.TransactionInfoDTO;
+import com.table.transfer.module.enpty.TransferRequest;
+import com.table.transfer.module.enpty.TransferResponse;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public interface BlockchainService {
+    TransferResponse transfer(TransferRequest request) throws Exception;
+
+    boolean validateAddress(String address);
+
+    void validateTransferRequest(TransferRequest transferRequest) throws Exception;
+
+    BigInteger getNonce(String assetCode, String address) throws Exception;
+
+    BigDecimal getTokenBalance(String chain, String contractAddress, String targetAddress) throws Exception;
+
+    BigDecimal getBalance(String chain, String targetAddress) throws Exception;
+
+    TransactionInfoDTO getTransactionInfoByHash(String assetCode, String transactionHash) throws Exception;
+}

+ 523 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/service/EthereumService.java

@@ -0,0 +1,523 @@
+package com.table.transfer.module.service;
+
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.table.transfer.module.enpty.ChainCoinDetail;
+import com.table.transfer.module.enpty.TransactionInfoDTO;
+import com.table.transfer.module.enpty.TransferRequest;
+import com.table.transfer.module.enpty.TransferResponse;
+import com.table.transfer.util.CryptoUtil;
+import com.table.transfer.util.aUtil;
+import com.table.transfer.util.ether.ChainCoinDetailManager;
+import com.table.transfer.util.ether.Web3jManager;
+import com.table.transfer.util.encryp.RSAEncryptionUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.web3j.abi.FunctionEncoder;
+import org.web3j.abi.FunctionReturnDecoder;
+import org.web3j.abi.TypeReference;
+import org.web3j.abi.datatypes.Address;
+import org.web3j.abi.datatypes.Bool;
+import org.web3j.abi.datatypes.Function;
+import org.web3j.abi.datatypes.Type;
+import org.web3j.abi.datatypes.generated.Uint256;
+import org.web3j.crypto.Credentials;
+import org.web3j.crypto.RawTransaction;
+import org.web3j.crypto.TransactionEncoder;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.DefaultBlockParameterName;
+import org.web3j.protocol.core.methods.request.Transaction;
+import org.web3j.protocol.core.methods.response.*;
+import org.web3j.utils.Convert;
+import org.web3j.utils.Numeric;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.rmi.ServerException;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+public class EthereumService implements BlockchainService {
+
+    //EVM 链上的 ERC20 固定的事件签名keccak256 哈希
+    String TRANSFER_EVENT_SIGNATURE = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
+
+    // 默认 Gas Limit(保险值)
+    public static final BigInteger DEFAULT_GAS_LIMIT_NATIVE = BigInteger.valueOf(21_000);
+    public static final BigInteger DEFAULT_GAS_LIMIT_TOKEN = BigInteger.valueOf(120_000);
+    // BSC:官方显示平均手续费,后续应该在数据库中配置,2021年时出现过手续费暴涨的情况,手动配置可以提高最低值,提高成功率
+
+    // 全局 nonce 锁,到期自动释放
+    private final Cache<String, Object> nonceLocks = CacheBuilder.newBuilder()
+            .expireAfterAccess(1, TimeUnit.HOURS) // 1小时无访问自动移除
+            .build();
+    private final Map<String, Object> pendingTransfers = new ConcurrentHashMap<>();
+
+
+    /**
+     * 获取当前网络 gas price,并设置“最低安全值”
+     */
+    public BigInteger getSafeGasPrice(TransferRequest transferRequest) throws IOException {
+        BigInteger networkGasPrice = Web3jManager.getWeb3j(transferRequest.getChain()).ethGasPrice().send().getGasPrice();
+        ChainCoinDetail chainCoinDetail = ChainCoinDetailManager.getChainCoinDetail(transferRequest.getAssetCode());
+        //设置的最低值
+        BigInteger minGasPrice = chainCoinDetail.getMinGasPrice();
+        //低于最低手续费,就取最低值
+        if (networkGasPrice.compareTo(minGasPrice) <= 0) {
+            networkGasPrice = minGasPrice;
+        } else {
+            //设定的最低值就直接加价20%防止拥堵被抛弃
+            networkGasPrice = networkGasPrice.multiply(BigInteger.valueOf(12)).divide(BigInteger.TEN);
+        }
+
+        BigInteger maxGasPrice = chainCoinDetail.getMaxGasPrice();
+        if (networkGasPrice.compareTo(maxGasPrice) > 0) {
+            throw new ServerException("手续费超过设定最大值,当前估算Price"+networkGasPrice);
+        }
+        return networkGasPrice;
+    }
+
+
+
+    public void validateTransferRequest(TransferRequest transferRequest) throws Exception {
+        String pk = RSAEncryptionUtil.decrypt(transferRequest.getEncryptionPrivateKey());
+        Credentials credentials = Credentials.create(pk);
+        String fromAddress = credentials.getAddress();
+        if (!transferRequest.getFromAddress().equalsIgnoreCase(fromAddress)) {
+            throw new Exception("私钥和from地址不一致");
+        }
+        transferRequest.setPrivateKey(pk);
+        // 验证私钥
+        if (!CryptoUtil.validatePrivateKey(transferRequest.getPrivateKey())) {
+            throw new Exception("无效的私钥格式");
+        }
+
+        // 验证地址
+        if (!CryptoUtil.validateEthAddress(transferRequest.getToAddress())) {
+            throw new Exception("无效的接收地址");
+        }
+        if (ChainCoinDetailManager.getChainCoinDetail(transferRequest.getAssetCode()) == null) {
+            throw new Exception("不支持" + transferRequest.getAssetCode());
+        }
+        //todo 支持的类型校验 合约
+    }
+
+
+    @Override
+    public TransferResponse transfer(TransferRequest transferRequest) throws Exception {
+        validateTransferRequest(transferRequest);
+        //交易对
+        String assetCode = transferRequest.getAssetCode();
+        //接受地址
+        String toAddress = transferRequest.getToAddress();
+        //金额
+        String amount = transferRequest.getAmount();
+        //使用对应链的web3j
+        Web3j web3j = Web3jManager.getWeb3j(transferRequest.getChain());
+        //私钥
+        String privateKey = transferRequest.getPrivateKey();
+        //是否代币转账
+        boolean isTokenTransfer = transferRequest.isTokenTransfer();
+        //发送地址
+        String fromAddress = transferRequest.getFromAddress();
+        //合约地址
+        String contractAddress = transferRequest.getContractAddress();
+        //代币配置
+        ChainCoinDetail chainCoinDetail = ChainCoinDetailManager.getChainCoinDetail(assetCode);
+
+        Credentials credentials = Credentials.create(privateKey);
+        //重试次数
+        int retryCount = 0;
+        //hash
+        String sentTxHash = null;
+
+        try {
+            //todo chainId和链检查 必须相同,避免跨链丢币风险。
+
+            //4. 转换金额为wei
+            BigInteger decimalsAmount = toWei(transferRequest.getAmount(), chainCoinDetail.getDecimal());
+            if (decimalsAmount.compareTo(BigInteger.ONE) < 0) {
+                return TransferResponse.error("转账金额过低");
+            }
+            //2.上锁获取nonce
+            BigInteger nonce = null;
+            if (pendingTransfers.containsKey(fromAddress)) {
+                throw new Exception("该发送地址已有未完成转账");
+            }
+            pendingTransfers.put(fromAddress, true);
+
+            Object lock = nonceLocks.get(fromAddress, Object::new);
+            nonce = getNonce(assetCode, fromAddress);
+            while (retryCount < 3) {
+                try {
+
+                    synchronized (lock) {
+                        log.info("【转账开始】账户={},目标={}, 金额={}, nonce={}", fromAddress, toAddress, amount, nonce);
+                        //5. 构建交易
+                        RawTransaction rawTransaction;
+                        //3.估算gas和gaslimit 估算个屁,limit和price估算完全不靠谱
+//                        BigInteger gasLimit = estimateGasSafely(transferRequest);
+                        BigInteger gasLimit= isTokenTransfer ? DEFAULT_GAS_LIMIT_TOKEN : DEFAULT_GAS_LIMIT_NATIVE;
+                        BigInteger currentGasPrice = getSafeGasPrice(transferRequest);
+                        log.info("gasLimit: {},currentGasPrice {}", gasLimit, currentGasPrice);
+                        //代币转账
+                        if (isTokenTransfer) {
+                            // 构造代币转账的 function 调用
+                            Function function = new Function(
+                                    "transfer",
+                                    Arrays.asList(new Address(toAddress), new Uint256(decimalsAmount)),
+                                    Arrays.asList(new TypeReference<Bool>() {
+                                    })
+                            );
+                            String data = FunctionEncoder.encode(function);
+
+                            log.info("【代币转账开始】发送者={}, 接收者={}, 代币合约={}, 金额={}, nonce={}", fromAddress, toAddress, contractAddress, decimalsAmount, nonce);
+                            // 构造 RawTransaction:目标是代币合约地址
+                            rawTransaction = RawTransaction.createTransaction(
+                                    nonce,
+                                    currentGasPrice,
+                                    gasLimit,
+                                    transferRequest.getContractAddress(),
+                                    BigInteger.ZERO,
+                                    data
+                            );
+                        } else {
+                            //主币转账
+                            log.info("【转账开始】账户={},目标={}, 金额={}, nonce={}", fromAddress, toAddress, amount, nonce);
+                            rawTransaction = RawTransaction.createEtherTransaction(
+                                    nonce,
+                                    currentGasPrice,
+                                    gasLimit,
+                                    toAddress,
+                                    decimalsAmount
+                            );
+                        }
+
+                        //6.签名交易
+                        byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, chainCoinDetail.getChainId(), credentials);
+                        String hexValue = Numeric.toHexString(signedMessage);
+                        //7.发送交易
+                        EthSendTransaction response = web3j.ethSendRawTransaction(hexValue).send();
+
+                        if (response.hasError()) {
+                            String errorMsg = response.getError().getMessage();
+                            // 仅对 underpriced 错误重试(同一 nonce)
+                            if (errorMsg.contains("underpriced") || errorMsg.contains("replacement transaction underpriced")) {
+                                retryCount++;
+                                log.warn(" 交易因手续费太低被拒,nonce={},第 {}/3 次重试,gasPrice={}",
+                                        nonce, retryCount, currentGasPrice);
+
+                                // 根据重试次数动态提高 gasPrice
+                                currentGasPrice = currentGasPrice.multiply(BigInteger.valueOf(12)).divide(BigInteger.valueOf(10)); // +20%
+
+                                // 不超过链上限
+                                if (currentGasPrice.compareTo(chainCoinDetail.getMaxGasPrice()) > 0) {
+                                    //todo 超过了链的上限,直接抛弃,然后修改数据库错误提示,告知是因为手续费太高导致的,避免大额手续费浪费
+                                    // 更新数据库
+                                    break;
+                                }
+
+                                continue; // 重试(同一 nonce)
+                            } else {
+                                // 其他错误直接抛出,如 nonce too low, insufficient funds, already known
+                                log.error("节点拒绝交易(不可重试): {}", errorMsg);
+                                throw new Exception("交易失败: " + errorMsg);
+                            }
+                        }
+
+                        // 8.成功发送,拿到哈希
+                        sentTxHash = response.getTransactionHash();
+                    }
+                    log.info("交易已广播,TxHash={}", sentTxHash);
+                    // 不再重试发送,直接等待确认
+                    break;
+
+                } catch (IOException e) {
+                    // 网络层异常:可能是超时,但交易可能已在链上
+                    if (sentTxHash != null) {
+                        TransactionInfoDTO transactionInfoByHash = getTransactionInfoByHash(assetCode, sentTxHash);
+                        if (transactionInfoByHash != null) {
+                            log.warn("网络异常,但交易已发送,尝试查询链上状态: {}", sentTxHash);
+                        }
+                        break; // 跳出重试,进入等待
+                    } else {
+                        // 连广播都没完成,且不是 underpriced 错误,不重试
+                        log.error("网络异常且无交易哈希,放弃重试", e);
+                        throw new Exception("网络异常,无法发送交易" + e);
+                    }
+                } catch (Exception e) {
+                    log.error("交易过程发生未预期异常", e);
+                    throw e;
+
+                }
+            }
+
+            if (sentTxHash == null) {
+                // 说明 3 次都失败了,且没拿到哈希
+                log.error("交易发送失败:尝试 3 次均被拒绝(gasPrice 过低),nonce={}", nonce);
+                // 注意:这里不能抛“可重试”异常,否则外部可能再调用
+                throw new Exception("交易因手续费太低被拒,已尝试3次,请稍后重试");
+            }
+            log.info("交易已发送,TxHash: {}", sentTxHash);
+
+//            // 11. 立即验证是否进入 mempool?
+//            if (!isTransactionInMempool(web3j, sentTxHash)) {
+//                log.error("交易未立即进入 mempool,可能广播失败: {}", sentTxHash);
+//            }
+            return TransferResponse.success(sentTxHash);
+
+        } catch (Exception e) {
+            log.error("ETH转账失败: {}", e.getMessage());
+            return TransferResponse.error("ETH转账失败: " + e.getMessage());
+        } finally {
+            pendingTransfers.remove(fromAddress);
+        }
+    }
+
+    /**
+     * 获取gasLimit
+     *
+     * @param request
+     * @return
+     */
+    public BigInteger estimateGasSafely(TransferRequest request) {
+        String chain = request.getChain();
+        boolean isTokenTransfer = request.isTokenTransfer();
+        String fromAddress = request.getFromAddress();
+        String toAddress = request.getToAddress();
+        String contractAddress = request.getContractAddress();
+        BigInteger amount = request.getAmountWei();
+        Web3j web3j = Web3jManager.getWeb3j(chain);
+        Transaction transaction;
+
+        try {
+            if (isTokenTransfer) {
+                // 代币转账:调用合约的 transfer(to, amount)
+                Function function = new Function(
+                        "transfer",
+                        Arrays.asList(
+                                new Address(toAddress),
+                                new Uint256(amount != null ? amount : BigInteger.ONE)
+                        ),
+                        List.of()
+                );
+                String data = FunctionEncoder.encode(function);
+                transaction = Transaction.createEthCallTransaction(fromAddress, contractAddress, data);
+            } else {
+                // 主币转账:data 为空
+                transaction = Transaction.createEthCallTransaction(fromAddress, toAddress, "", BigInteger.ONE);
+            }
+
+            EthEstimateGas estimate;
+
+            try {
+                estimate = web3j.ethEstimateGas(transaction).send();
+                BigInteger gasUsed = estimate.getAmountUsed();
+
+                return gasUsed;
+            } catch (Exception e) {
+                //删掉重试机制,减少响应时间,失败直接返回默认值
+                log.error("Gas 估算失败{}", e.getMessage());
+            }
+
+            // 兜底:返回默认值
+            BigInteger defaultValue = (transaction.getData() == null || transaction.getData().isEmpty()) ?
+                    DEFAULT_GAS_LIMIT_NATIVE : // 主币
+                    DEFAULT_GAS_LIMIT_TOKEN;   // 代币
+
+            log.error("使用保险默认 Gas Limit: {}", defaultValue);
+            return defaultValue;
+
+        } catch (Exception e) {
+            log.error("Gas 估算准备阶段异常: {}", e.getMessage());
+            e.printStackTrace();
+            // 返回基于链类型的默认值
+            return isTokenTransfer ? DEFAULT_GAS_LIMIT_TOKEN : DEFAULT_GAS_LIMIT_NATIVE;
+        }
+    }
+
+
+    /**
+     * 获取nonce/交易总数
+     *
+     * @return
+     * @throws Exception
+     */
+    public BigInteger getNonce(String assetCode, String address) throws Exception {
+        Web3j web3j = Web3jManager.getWeb3j(aUtil.assetCode2Chain(assetCode));
+        EthGetTransactionCount ethGetTransactionCount = web3j
+                .ethGetTransactionCount(address, DefaultBlockParameterName.PENDING)
+                .send();
+        return ethGetTransactionCount.getTransactionCount();
+
+    }
+
+
+    private BigInteger toWei(String amount, Integer decimal) throws Exception {
+        // 简化的转换,实际需要处理小数
+        BigDecimal pow = BigDecimal.TEN.pow(decimal);
+        return new BigDecimal(amount).multiply(pow).toBigInteger();
+
+    }
+
+
+    /**
+     * 目标地址代币余额
+     */
+    public BigDecimal getTokenBalance(String assetCode, String contractAddress, String targetAddress) throws Exception {
+
+        Function function = new Function("balanceOf",
+                List.of(new Address(targetAddress)),
+                List.of(new TypeReference<Uint256>() {
+                })
+        );
+
+        String data = FunctionEncoder.encode(function);
+        Transaction transaction = Transaction.createEthCallTransaction(null, contractAddress, data);
+
+        EthCall response = Web3jManager.getWeb3j(aUtil.assetCode2Chain(assetCode)).ethCall(transaction, DefaultBlockParameterName.LATEST).send();
+        log.info("EthCall response: {}", JSONUtil.toJsonStr(response));
+        List<Type> result = FunctionReturnDecoder.decode(response.getValue(), function.getOutputParameters());
+
+        if (result.isEmpty()) {
+            throw new Exception("获取代币余额失败");
+        }
+
+        BigInteger balanceWei = (BigInteger) result.get(0).getValue();
+        log.info("Raw balance: {}", balanceWei);
+
+        int decimals = ChainCoinDetailManager.getChainCoinDetail(assetCode).getDecimal(); // 如果需要精确,应该调用合约的 decimals() 方法
+        BigDecimal balance = new BigDecimal(balanceWei)
+                .divide(new BigDecimal(BigInteger.TEN.pow(decimals)), decimals, RoundingMode.DOWN);
+
+        return balance;
+    }
+
+
+    /**
+     * 获取目标地址代主币余额
+     *
+     * @param targetAddress
+     * @return
+     * @throws Exception
+     */
+    public BigDecimal getBalance(String chain, String targetAddress) throws Exception {
+
+        EthGetBalance balanceResponse = Web3jManager.getWeb3j(chain).ethGetBalance(targetAddress, DefaultBlockParameterName.LATEST).send();
+        Convert.Unit unit;
+        if (chain.equalsIgnoreCase("ETH") || chain.equalsIgnoreCase("BSC")) {
+            unit = Convert.Unit.ETHER;
+        } else {
+            throw new Exception("暂不支持的链");
+        }
+        return Convert.fromWei(balanceResponse.getBalance().toString(), unit);
+    }
+
+    public Long getLastBlock(String chain) throws IOException {
+        EthBlockNumber blockNumber = Web3jManager.getWeb3j(chain).ethBlockNumber().send();
+        return blockNumber.getBlockNumber().longValue();
+    }
+
+
+    /**
+     * 校验地址有效性
+     *
+     * @param address
+     * @return
+     */
+    @Override
+    public boolean validateAddress(String address) {
+        return CryptoUtil.validateEthAddress(address);
+    }
+
+    /**
+     * 通过hash获取交易信息
+     */
+    @Override
+    public TransactionInfoDTO getTransactionInfoByHash(String assetCode, String transactionHash) throws Exception {
+        String chain = aUtil.assetCode2Chain(assetCode);
+        // 查询交易回执
+        EthGetTransactionReceipt receiptResponse = Web3jManager.getWeb3j(chain).ethGetTransactionReceipt(transactionHash).send();
+        Optional<TransactionReceipt> receiptOpt = receiptResponse.getTransactionReceipt();
+        if (!receiptOpt.isPresent()) {
+            log.warn("交易尚未上链: {}", transactionHash);
+            throw new ServerException("交易尚未上链: " + transactionHash);
+        }
+
+        TransactionReceipt receipt = receiptOpt.get();
+        // 必须有区块号(已上链)
+        if (receipt.getBlockNumber() == null) {
+            log.warn("交易回执无区块号: {}", transactionHash);
+            throw new ServerException("交易回执无区块号: " + transactionHash);
+        }
+
+        //判断交易是否执行成功
+        boolean isSuccess = "0x1".equals(receipt.getStatus()) || "1".equals(receipt.getStatus());
+
+        if (!isSuccess) {
+            log.warn("交易执行失败 (reverted): {}, status={}", transactionHash, receipt.getStatus());
+            throw new ServerException(StrUtil.format("交易执行失败 (reverted): {}, status={}", transactionHash, receipt.getStatus()));
+        }
+        TransactionReceipt transactionReceipt = receiptOpt.orElseThrow(RuntimeException::new);
+
+
+        // 解析 logs 获取真正的 to 地址和 amount
+        List<Log> logs = transactionReceipt.getLogs();
+        if (!logs.isEmpty()) {
+
+            for (Log log : logs) {
+                //确保有3个主题并且符合keccak256
+                if (log.getTopics().size() >= 3 && TRANSFER_EVENT_SIGNATURE.equals(log.getTopics().get(0))) {
+
+                    // 主题解析:主题0是事件签名,主题1是from,主题2是to
+                    List<String> topics = log.getTopics();
+                    String transferFrom = Numeric.toBigInt(topics.get(1)).toString(16);
+                    transferFrom = "0x" + padZeroes(transferFrom, 40); // 补齐40位(20字节)
+
+                    String transferTo = Numeric.toBigInt(topics.get(2)).toString(16);
+                    transferTo = "0x" + padZeroes(transferTo, 40); // 补齐40位
+
+
+                    String data = log.getData();
+                    BigInteger amountValue = Numeric.toBigInt(data);
+
+                    //计算gas费
+                    BigInteger gasUsed = Numeric.toBigInt(transactionReceipt.getGasUsed().toByteArray());
+                    BigInteger effectiveGasPrice = Numeric.toBigInt(transactionReceipt.getEffectiveGasPrice());
+                    BigInteger totalGasFeeWei = gasUsed.multiply(effectiveGasPrice);
+                    // todo eth和bsc位数一样,后续增加其他EVM币种若是位数不一样这里需要改适配
+                    BigDecimal totalGasFeeEth = Convert.fromWei(totalGasFeeWei.toString(), Convert.Unit.ETHER);
+
+                    TransactionInfoDTO transactionInfoDTO = new TransactionInfoDTO();
+                    transactionInfoDTO.setStatus("success");
+
+                    Integer decimal = ChainCoinDetailManager.getChainCoinDetail(assetCode).getDecimal();
+                    // USDT 是 6 位小数,所以除以 10^6
+
+                    BigDecimal amount = new BigDecimal(amountValue).divide(new BigDecimal(BigInteger.TEN.pow(decimal)), decimal, RoundingMode.DOWN);
+                    transactionInfoDTO.setAmount(amount);
+                    transactionInfoDTO.setGasFee(totalGasFeeEth);
+                    transactionInfoDTO.setHash(transactionReceipt.getTransactionHash());
+                    transactionInfoDTO.setFromAddress(transferFrom);
+                    transactionInfoDTO.setToAddress(transferTo);
+                    return transactionInfoDTO;
+                }
+            }
+        }
+        throw new Exception("ethscan解析Hash失败");
+    }
+
+    private static String padZeroes(String hex, int length) {
+        if (hex.length() >= length) return hex;
+        return "0".repeat(length - hex.length()) + hex;
+    }
+
+}

+ 12 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/service/Impl/TransferRecordServiceImpl.java

@@ -0,0 +1,12 @@
+package com.table.transfer.module.service.Impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.table.transfer.module.enpty.TransferRecord;
+import com.table.transfer.module.mapper.TransferRecordMapper;
+import com.table.transfer.module.service.TransferRecordService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TransferRecordServiceImpl extends ServiceImpl<TransferRecordMapper, TransferRecord> implements TransferRecordService {
+
+}

+ 8 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/service/TransferRecordService.java

@@ -0,0 +1,8 @@
+package com.table.transfer.module.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.table.transfer.module.enpty.TransferRecord;
+
+public interface TransferRecordService extends IService<TransferRecord> {
+
+}

+ 485 - 0
blockchain-transfer/src/main/java/com/table/transfer/module/service/TronService.java

@@ -0,0 +1,485 @@
+package com.table.transfer.module.service;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.util.concurrent.RateLimiter;
+import com.table.transfer.module.enpty.TransactionInfoDTO;
+import com.table.transfer.module.enpty.TransferRequest;
+import com.table.transfer.module.enpty.TransferResponse;
+import com.table.transfer.util.encryp.RSAEncryptionUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.util.encoders.Hex;
+import org.springframework.stereotype.Service;
+import org.tron.trident.core.ApiWrapper;
+import org.tron.trident.core.contract.Contract;
+import org.tron.trident.core.contract.Trc20Contract;
+import org.tron.trident.crypto.Hash;
+import org.tron.trident.proto.Chain;
+import org.tron.trident.proto.Response;
+import org.tron.trident.utils.Base58Check;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.rmi.ServerException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+
+@Slf4j
+@Service
+public class TronService {
+
+
+    // tronGrid API配置
+    private static final String API_KEY = "0ab62ce0-04bd-45e8-9570-db636c19b6df";
+    /**
+     * 租赁一次能量需要的trx
+     */
+    public static final long DEFAULT_LEASE_AMOUNT_SUN = 2_500_000L; // 2.5 TRX
+    /**
+     * 能量租赁地址
+     */
+    public static final String DEFAULT_ENERGY_LEASE_ADDRESS = "TDCaC2PcAWenMv6ErDUq847ENHMtufud2H";
+
+    /**
+     * 能量租赁等待时间上限
+     */
+    private static final long ENERGY_WAIT_TIMEOUT_MS = 120_000L;
+    /**
+     * 地址+合约,余额(健康检查的时候顺便加载)
+     */
+    private final Cache<String, BigDecimal> addressBalance = CacheBuilder.newBuilder()
+            .expireAfterAccess(1, TimeUnit.HOURS)
+            .build();
+
+    /**
+     * trx 精度
+     */
+    private static final BigDecimal SUN_TO_TRX = BigDecimal.valueOf(1_000_000);
+    /**
+     * 单词转账的能量阈值
+     */
+    private static final long ENERGY_PER_TRANSFER = 65_000L;
+    /**
+     * 转账地址锁,todo mq保证串行后可删
+     */
+    private final Map<String, Object> transferLocks = new ConcurrentHashMap<>();
+
+    /**
+     * 频率限制器
+     */
+    private final RateLimiter ApiWrapperLimiter = RateLimiter.create(2);
+    /**
+     * ApiWrapper 连接异常的重试次数
+     */
+    private int heathApiWrapperCount = 0;
+    /**
+     * ApiWrapper 连接异常的最大重试次数
+     */
+    private final int MaxHeathApiWrapperCount = 20;
+
+    /**
+     * 配合限制器限制请求频率
+     */
+    private ApiWrapper limitApiWrapper(ApiWrapper apiWrapper) {
+        ApiWrapperLimiter.acquire();
+        return apiWrapper;
+    }
+
+    /**
+     * 获取一个健康可用的ApiWrapper
+     */
+    public ApiWrapper heathApiWrapper(String privateKey, String fromAddress, String trc20Contract) {
+        ApiWrapper apiWrapper = null;
+        while (heathApiWrapperCount < MaxHeathApiWrapperCount) {
+            try {
+                apiWrapper = ApiWrapper.ofMainnet(privateKey, API_KEY);
+                //健康检查,顺便更新余额
+                long trxBalance = limitApiWrapper(apiWrapper).getAccountBalance(fromAddress);
+                BigDecimal trxInTrx = new BigDecimal(trxBalance).divide(new BigDecimal(1_000_000), 6, RoundingMode.HALF_UP);
+                addressBalance.put(fromAddress, trxInTrx);
+
+                Contract contract = limitApiWrapper(apiWrapper).getContract(trc20Contract);
+                Trc20Contract token = new Trc20Contract(contract, fromAddress, limitApiWrapper(apiWrapper));
+                BigInteger balanceOf = token.balanceOf(fromAddress);
+                // todo 管理代币精度
+                BigDecimal divisor = new BigDecimal(1000000);
+                BigDecimal divide = new BigDecimal(balanceOf).divide(divisor, 8, RoundingMode.HALF_UP);
+                addressBalance.put(fromAddress + trc20Contract, divide);
+                heathApiWrapperCount = 0;
+                return apiWrapper;
+            } catch (Exception e) {
+                apiWrapper.close();
+                log.warn("ApiWrapper连接异常{},尝试第{}次重新连接", e.getMessage(), MaxHeathApiWrapperCount);
+                heathApiWrapperCount++;
+            }
+        }
+        throw new RuntimeException("ApiWrapper连接异常,全部重试都已失败 heathApiWrapperCount:" + heathApiWrapperCount);
+
+    }
+
+    /**
+     * 转账服务
+     */
+    public TransferResponse transfer(TransferRequest request) throws Exception {
+        validateTransferRequest(request);
+        //私钥
+        String privateKey = request.getPrivateKey();
+        //目标地址
+        String toAddress = request.getToAddress();
+        //金额
+        String amount = request.getAmount();
+        //发送地址
+        String fromAddress = request.getFromAddress();
+        //是否代币转账
+        boolean isTokenTransfer = request.isTokenTransfer();
+        //合约地址
+        String contractAddress = request.getContractAddress();
+        //mq串行,无并发
+        ApiWrapper apiWrapper = null;
+        try {
+            //获取检查过后的apiWrapper
+            apiWrapper = heathApiWrapper(privateKey, fromAddress, contractAddress);
+
+            // 验证私钥和地址匹配
+            String derivedAddress = limitApiWrapper(apiWrapper).keyPair.toBase58CheckAddress();
+            if (!fromAddress.equalsIgnoreCase(derivedAddress)) {
+                throw new RuntimeException("私钥和发送地址不匹配");
+            }
+
+            String txId;
+            if (isTokenTransfer) {
+                //代币转账
+                txId = transferToken(apiWrapper, fromAddress, toAddress, contractAddress, amount);
+            } else {
+                //主币转账
+                txId = transferTrx(apiWrapper, fromAddress, toAddress, amount);
+            }
+
+            log.info("TRON交易已发送,交易ID: {}", txId);
+            return TransferResponse.success(txId);
+
+        } catch (Exception e) {
+            log.error("TRON转账失败: {}", e.getMessage(), e);
+            return TransferResponse.error("TRON转账失败: " + e.getMessage());
+        } finally {
+            apiWrapper.close();
+            transferLocks.remove(fromAddress);
+        }
+
+    }
+
+    /**
+     * TRX 主币转账
+     */
+    private String transferTrx(ApiWrapper apiWrapper, String fromAddress, String toAddress, String amount) throws Exception {
+        try {
+            long amountInSun = new BigDecimal(amount).multiply(SUN_TO_TRX).longValue();
+
+            if (amountInSun <= 0) {
+                throw new RuntimeException("转账金额必须大于0");
+            }
+
+            // 检查主币余额
+            BigDecimal balance = addressBalance.getIfPresent(fromAddress);
+            if (balance == null) {
+                balance = getBalance(apiWrapper, fromAddress);
+            }
+
+            BigDecimal requiredBalance = new BigDecimal(amount);
+            if (balance.compareTo(requiredBalance) < 0) {
+                throw new RuntimeException("TRX余额不足,当前余额: " + balance + " TRX,需要: " + requiredBalance + " TRX");
+            }
+
+            log.info("开始TRX转账: {} -> {}, 金额: {} TRX ({} SUN)", fromAddress, toAddress, amount, amountInSun);
+
+            Response.TransactionExtention transactionExtention = limitApiWrapper(apiWrapper).transfer(fromAddress, toAddress, amountInSun);
+
+            if (!transactionExtention.getResult().getResult()) {
+                throw new RuntimeException("交易构建失败: " + transactionExtention.getResult().getCode());
+            }
+
+            Chain.Transaction transaction = transactionExtention.getTransaction();
+            Chain.Transaction signedTx = limitApiWrapper(apiWrapper).signTransaction(transaction);
+            return limitApiWrapper(apiWrapper).broadcastTransaction(signedTx);
+//            return "测试503hash";
+        } catch (Exception e) {
+            log.error("TRX转账失败: {}", e.getMessage(), e);
+            throw new RuntimeException("TRX转账失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * TRC20 代币转账
+     */
+    private String transferToken(ApiWrapper apiWrapper, String fromAddress, String toAddress,
+                                 String contractAddress, String amount) throws Exception {
+        String txHash = null;
+        try {
+
+            // 1. 检查代币余额
+            BigDecimal tokenBalance = addressBalance.getIfPresent(fromAddress + contractAddress);
+            if (tokenBalance == null) {
+                tokenBalance = getTokenBalance(apiWrapper, contractAddress, fromAddress);
+            }
+            BigDecimal transferAmount = new BigDecimal(amount);
+            if (tokenBalance.compareTo(transferAmount) < 0) {
+                throw new RuntimeException("代币余额不足,当前余额: " + tokenBalance + ",需要: " + transferAmount);
+            }
+
+
+            // 2. 计算需要的能量笔数
+            //todo 没有U需要2笔能量,有的需要1笔 ,但是转账其他代币不知道是不是也是以USDT为基数的
+            int energyCount = calculateRequiredEnergyCount(apiWrapper, contractAddress, toAddress);
+            long requiredEnergy = ENERGY_PER_TRANSFER * energyCount;
+            log.info("代币转账需要 {} 笔能量,总计 {} 能量", energyCount, requiredEnergy);
+
+            // 3. 检查当前能量是否足够
+            long currentEnergy = getAvailableEnergy(apiWrapper, fromAddress);
+            if (currentEnergy < requiredEnergy) {
+                log.info("能量不足: {} < {},需要租赁 {} 笔能量", currentEnergy, requiredEnergy, energyCount);
+                // 租赁精确数量的能量
+                leaseEnergy(apiWrapper, fromAddress, energyCount);
+                // 等待能量到账
+                waitForEnergyReplenishment(apiWrapper, fromAddress, requiredEnergy);
+            } else {
+                log.info("能量充足: {} >= {},直接进行转账", currentEnergy, requiredEnergy);
+            }
+
+            // 4. 执行代币转账
+            BigInteger amountInSmallestUnit = new BigDecimal(amount)
+                    .multiply(BigDecimal.TEN.pow(6)) // 默认6位小数
+                    .toBigInteger();
+
+            if (amountInSmallestUnit.compareTo(BigInteger.ZERO) <= 0) {
+                throw new RuntimeException("转账金额必须大于0");
+            }
+
+            log.info("开始TRC20转账: {} -> {}, 合约: {}, 金额: {}, 能量笔数: {}", fromAddress, toAddress, contractAddress, amount, energyCount);
+
+            // 使用Trc20Contract进行转账
+            Contract contract = limitApiWrapper(apiWrapper).getContract(contractAddress);
+            Trc20Contract token = new Trc20Contract(contract, fromAddress, limitApiWrapper(apiWrapper));
+            txHash = token.transfer(toAddress, amountInSmallestUnit.longValue(), 0, "", 1000000000L);
+
+            log.info("代币转账广播成功: txId={}", txHash);
+
+            return txHash;
+
+        } catch (Exception e) {
+            log.error("TRC20代币转账失败: {}", e.getMessage(), e);
+
+            // 如果交易已广播但后续出错,检查是否已确认
+            if (txHash != null && isTransactionConfirmed(apiWrapper, txHash)) {
+                log.info("检测到交易已上链,尽管本地异常: {}", txHash);
+                return txHash;
+            }
+
+            throw new RuntimeException("TRC20代币转账失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 循环等待能量补充
+     */
+    public void waitForEnergyReplenishment(ApiWrapper apiWrapper, String address, long requiredEnergy) throws Exception {
+        long startTime = System.currentTimeMillis();
+
+        while (System.currentTimeMillis() - startTime < ENERGY_WAIT_TIMEOUT_MS) {
+            try {
+                long availableEnergy = getAvailableEnergy(apiWrapper, address);
+                if (availableEnergy >= requiredEnergy) {
+                    log.info("能量已补充至 {}", availableEnergy);
+                    return;
+                }
+                log.info("当前能量 {},需要 {},继续等待...", availableEnergy, requiredEnergy);
+                Thread.sleep(3000);
+            } catch (Exception e) {
+                log.warn("查询能量失败: {}", e.getMessage());
+                Thread.sleep(3000);
+            }
+        }
+        throw new RuntimeException("能量租赁未到账,超时");
+    }
+
+    /**
+     * 计算需要的能量笔数
+     * 目标地址有代币:1笔能量
+     * 目标地址无代币:2笔能量
+     */
+    private int calculateRequiredEnergyCount(ApiWrapper apiWrapper, String contractAddress, String toAddress) throws Exception {
+        try {
+            BigDecimal targetBalance = getTokenBalance(apiWrapper, contractAddress, toAddress);
+            return targetBalance.compareTo(BigDecimal.ZERO) > 0 ? 1 : 2;
+        } catch (Exception e) {
+            log.warn("计算能量笔数失败,使用保守值2: {}", e.getMessage());
+            return 2; // 保守起见,使用2笔能量
+        }
+    }
+
+    /**
+     * 查询余额
+     */
+    public BigDecimal getBalance(ApiWrapper apiWrapper, String address) throws Exception {
+        try {
+            long balanceInSun = limitApiWrapper(apiWrapper).getAccountBalance(address);
+            return BigDecimal.valueOf(balanceInSun).divide(SUN_TO_TRX);
+        } catch (Exception e) {
+            log.error("获取主币余额失败: {}", address, e);
+            throw new ServerException("获取TRX余额失败: " + address);
+        }
+    }
+
+    /**
+     * 查询代币余额
+     */
+    public BigDecimal getTokenBalance(ApiWrapper apiWrapper, String contractAddress, String targetAddress) {
+        Contract contract = limitApiWrapper(apiWrapper).getContract(contractAddress);
+        Trc20Contract token = new Trc20Contract(contract, targetAddress, limitApiWrapper(apiWrapper));
+        BigInteger balanceOf = token.balanceOf(targetAddress);
+        //配置代币精度和小数点保留位数
+        BigDecimal divisor = new BigDecimal(1000000);
+        BigDecimal divide = new BigDecimal(balanceOf).divide(divisor, 8, RoundingMode.HALF_UP);
+        return divide;
+
+    }
+
+
+    /**
+     * 检查交易是否确认
+     */
+    private boolean isTransactionConfirmed(ApiWrapper apiWrapper, String txHash) {
+        try {
+            Response.TransactionInfo transactionInfo = limitApiWrapper(apiWrapper).getTransactionInfoById(txHash);
+            return transactionInfo != null;
+        } catch (Exception e) {
+            log.warn("检查交易确认状态失败: {}", txHash, e);
+            return false;
+        }
+    }
+
+
+    public TransactionInfoDTO getTransactionInfoByHash(ApiWrapper apiWrapper, String assetCode, String transactionHash) throws Exception {
+        try {
+            // 先尝试用ApiWrapper获取
+
+            Response.TransactionInfo transactionInfo = limitApiWrapper(apiWrapper).getTransactionInfoById(transactionHash);
+
+            if (transactionInfo != null) {
+                return buildTransactionInfoDTO(transactionInfo, assetCode);
+            }
+        } catch (Exception e) {
+            log.warn("ApiWrapper获取交易信息失败,降级到TronScan API: {}", transactionHash);
+        }
+        throw new Exception();
+        // 降级到TronScan API
+//        return getTransactionInfoFromTronScan(assetCode, transactionHash);
+    }
+
+    /**
+     * 构建交易信息DTO
+     */
+    private TransactionInfoDTO buildTransactionInfoDTO(Response.TransactionInfo transactionInfo, String assetCode) {
+        TransactionInfoDTO dto = new TransactionInfoDTO();
+        dto.setHash(transactionInfo.getId().toString());
+        dto.setStatus(transactionInfo.getResult().toString().toLowerCase());
+        dto.setBlockNumber(transactionInfo.getBlockNumber());
+        dto.setTimestamp(transactionInfo.getBlockTimeStamp());
+
+        // 设置手续费
+        BigDecimal feeInSun = new BigDecimal(transactionInfo.getFee());
+        BigDecimal feeInTrx = feeInSun.divide(SUN_TO_TRX);
+        dto.setGasFee(feeInTrx);
+
+        // 这里可以根据transactionInfo解析from/to/amount等详细信息
+        // 需要根据实际交易类型进行解析
+
+        return dto;
+    }
+
+    /**
+     * 验证地址格式
+     */
+    public boolean validateAddress(String address) {
+        if (address == null || address.length() < 20) return false;
+        try {
+            byte[] decoded = Base58Check.base58ToBytes(address);
+            return decoded.length == 21;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 转账前参数检查
+     */
+    public void validateTransferRequest(TransferRequest transferRequest) throws Exception {
+        String pk = RSAEncryptionUtil.decrypt(transferRequest.getEncryptionPrivateKey());
+
+        transferRequest.setPrivateKey(pk);
+
+        if (!validateAddress(transferRequest.getToAddress())) {
+            throw new Exception("无效的接收地址");
+        }
+        if (!validateAddress(transferRequest.getFromAddress())) {
+            throw new Exception("无效的发送地址");
+        }
+
+        try {
+            BigDecimal amount = new BigDecimal(transferRequest.getAmount());
+            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+                throw new Exception("转账金额必须大于0");
+            }
+        } catch (NumberFormatException e) {
+            throw new Exception("无效的金额格式");
+        }
+
+        if (transferRequest.isTokenTransfer() &&
+                (transferRequest.getContractAddress() == null || transferRequest.getContractAddress().isEmpty())) {
+            throw new Exception("代币转账需要合约地址");
+        }
+    }
+
+    /**
+     * 检查账户能量值
+     *
+     * @param address
+     * @return
+     * @throws Exception
+     */
+    public long getAvailableEnergy(ApiWrapper apiWrapper, String address) throws Exception {
+        try {
+            Response.AccountResourceMessage resource = limitApiWrapper(apiWrapper).getAccountResource(address);
+            return resource.getEnergyLimit() - resource.getEnergyUsed();
+        } catch (Exception e) {
+            log.error("获取账户能量失败: {}", address, e);
+            throw new RuntimeException("获取账户能量失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 租赁能量(精确笔数,无法叠加)
+     */
+    public void leaseEnergy(ApiWrapper apiWrapper, String fromAddress, int energyCount) throws Exception {
+        if (energyCount <= 0) {
+            throw new IllegalArgumentException("能量笔数必须大于0");
+        }
+
+        long leaseAmount = DEFAULT_LEASE_AMOUNT_SUN * energyCount;
+        log.info("租赁 {} 笔能量,金额: {} TRX from {} to {}", energyCount, BigDecimal.valueOf(leaseAmount).divide(BigDecimal.valueOf(1_000_000)), fromAddress, DEFAULT_ENERGY_LEASE_ADDRESS);
+
+        Response.TransactionExtention transfer = limitApiWrapper(apiWrapper).transfer(fromAddress, DEFAULT_ENERGY_LEASE_ADDRESS, leaseAmount);
+        Chain.Transaction signedTx = limitApiWrapper(apiWrapper).signTransaction(transfer);
+        String txId = limitApiWrapper(apiWrapper).broadcastTransaction(signedTx);
+
+        if (txId == null || txId.isEmpty()) {
+            throw new RuntimeException("能量租赁交易广播失败");
+        }
+
+        log.info("能量租赁交易广播成功: {}", txId);
+    }
+}

+ 162 - 0
blockchain-transfer/src/main/java/com/table/transfer/util/CryptoUtil.java

@@ -0,0 +1,162 @@
+package com.table.transfer.util;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.encoders.Hex;
+import org.web3j.abi.FunctionEncoder;
+import org.web3j.abi.TypeReference;
+import org.web3j.abi.datatypes.Address;
+import org.web3j.abi.datatypes.Function;
+import org.web3j.abi.datatypes.generated.Uint256;
+import org.web3j.crypto.*;
+import org.web3j.utils.Numeric;
+
+import java.math.BigInteger;
+import java.security.Security;
+import java.util.Arrays;
+import java.util.Collections;
+
+public class CryptoUtil {
+    
+    // secp256k1曲线的阶n
+    private static final BigInteger SECP256K1_N = new BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16);
+    
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+    
+    public static ECKeyPair createKeyPairFromPrivateKey(String privateKeyHex) {
+        try {
+            // 处理0x前缀
+            if (privateKeyHex.startsWith("0x")) {
+                privateKeyHex = privateKeyHex.substring(2);
+            }
+            
+            BigInteger privateKey = new BigInteger(privateKeyHex, 16);
+            return ECKeyPair.create(privateKey);
+        } catch (Exception e) {
+            throw new RuntimeException("无效的私钥: " + e.getMessage());
+        }
+    }
+    
+    public static String getAddressFromPrivateKey(String privateKeyHex) {
+        ECKeyPair keyPair = createKeyPairFromPrivateKey(privateKeyHex);
+        return Keys.getAddress(keyPair);
+    }
+    
+    public static String getChecksumAddress(String address) {
+        return Keys.toChecksumAddress(address);
+    }
+    
+    public static RawTransaction createEthTransaction(
+            BigInteger nonce, 
+            BigInteger gasPrice, 
+            BigInteger gasLimit, 
+            String toAddress,
+            BigInteger value, 
+            String data) {
+        
+        return RawTransaction.createTransaction(
+            nonce,
+            gasPrice,
+            gasLimit,
+            toAddress,
+            value,
+            data
+        );
+    }
+    
+    public static String signTransaction(RawTransaction rawTransaction, byte chainId, ECKeyPair keyPair) {
+        Credentials credentials = Credentials.create(keyPair);
+        byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials);
+        return Numeric.toHexString(signedMessage);
+    }
+    
+    public static String buildTokenTransferData(String toAddress, BigInteger value) {
+        try {
+            // 确保地址格式正确
+            String normalizedToAddress = toAddress;
+            if (normalizedToAddress.startsWith("0x")) {
+                normalizedToAddress = normalizedToAddress.substring(2);
+            }
+            
+            Function function = new Function(
+                "transfer",
+                Arrays.asList(
+                    new Address(normalizedToAddress),
+                    new Uint256(value)
+                ),
+                Collections.singletonList(new TypeReference<org.web3j.abi.datatypes.Bool>() {})
+            );
+            
+            return FunctionEncoder.encode(function);
+        } catch (Exception e) {
+            throw new RuntimeException("构建代币转账数据失败: " + e.getMessage());
+        }
+    }
+    
+    public static boolean validatePrivateKey(String privateKey) {
+        try {
+            if (privateKey.startsWith("0x")) {
+                privateKey = privateKey.substring(2);
+            }
+            
+            if (privateKey.length() != 64) {
+                return false;
+            }
+            
+            BigInteger pk = new BigInteger(privateKey, 16);
+            
+            // 检查私钥范围: 1 <= privateKey < n
+            return pk.compareTo(BigInteger.ONE) >= 0 && 
+                   pk.compareTo(SECP256K1_N) < 0;
+            
+        } catch (Exception e) {
+            return false;
+        }
+    }
+    
+    public static boolean validateEthAddress(String address) {
+        if (address == null || !address.matches("^0x[a-fA-F0-9]{40}$")) {
+            return false;
+        }
+        
+        // 简单的校验和验证
+        try {
+            String checksumAddress = Keys.toChecksumAddress(address);
+            return address.equals(checksumAddress) || 
+                   address.equalsIgnoreCase(checksumAddress);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+    
+    public static BigInteger weiToEther(BigInteger wei) {
+        return wei.divide(BigInteger.TEN.pow(18));
+    }
+    
+    public static BigInteger etherToWei(BigInteger ether) {
+        return ether.multiply(BigInteger.TEN.pow(18));
+    }
+    
+    public static BigInteger etherToWei(String etherAmount) {
+        try {
+            // 处理小数
+            String[] parts = etherAmount.split("\\.");
+            BigInteger integerPart = new BigInteger(parts[0]).multiply(BigInteger.TEN.pow(18));
+            
+            if (parts.length > 1) {
+                String decimalPart = parts[1];
+                if (decimalPart.length() > 18) {
+                    decimalPart = decimalPart.substring(0, 18);
+                }
+                BigInteger decimalValue = new BigInteger(decimalPart)
+                    .multiply(BigInteger.TEN.pow(18 - decimalPart.length()));
+                integerPart = integerPart.add(decimalValue);
+            }
+            
+            return integerPart;
+        } catch (Exception e) {
+            throw new RuntimeException("ETH金额格式错误: " + etherAmount);
+        }
+    }
+}

+ 431 - 0
blockchain-transfer/src/main/java/com/table/transfer/util/TronGridQueryUtil.java

@@ -0,0 +1,431 @@
+package com.table.transfer.util;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.table.transfer.module.enpty.TransactionInfoDTO;
+import com.table.transfer.util.tron.TronHexAndBase58Util;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+
+/**
+ * TRONGrid 查询工具类
+ * 封装独立查询功能,避免使用有问题的一次性接口
+ */
+public class TronGridQueryUtil {
+
+    private static final String TRONGRID_BASE_URL = "https://api.trongrid.io";
+    private static String apiKey = "8c5536e3-a852-43f6-add4-55421317f61c"; // 可选:设置你的 TRONGrid API Key
+    private static final String QUICKNODE_URL = "https://quaint-wild-layer.tron-mainnet.quiknode.pro/7d1ea336b14b5f53d8c897fe012aceec97a55fd7/";
+
+    /**
+     * 设置 TRONGrid API Key(可选)
+     */
+    public static void setApiKey(String key) {
+        apiKey = key;
+    }
+
+    public static void main(String[] args) {
+        String usdtContract = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
+        String address = "TDTAYsJDnUNCHJgPWLbyE1GutRJ5KBh1Bd";
+//        String address = "TDFtN7nyfweR8WeUraZM7WuCrtUTruD4Er";
+        System.out.println(getTRXBalance(address));
+        System.out.println(getTokenBalance(address, usdtContract,6));
+//
+//        System.out.println(JSONUtil.toJsonStr(getTransactionStatus("a8cd72046d1f6f60a86366cc006c7106037f7188a9bad5aca813389bf0cfec75")));
+
+    }
+
+    /**
+     * 1. 通过地址查询主币余额(TRX)
+     *
+     * @param address TRON地址
+     * @return TRX余额(单位:TRX)
+     */
+    public static BigDecimal getTRXBalance(String address) {
+        String url = TRONGRID_BASE_URL + "/wallet/getaccount";
+
+        JSONObject params = new JSONObject();
+        params.put("address", address);
+        params.put("visible", true);
+
+        try {
+            String response = HttpRequest.post(url)
+                    .header("Content-Type", "application/json")
+                    .timeout(10000)
+                    .body(params.toString())
+                    .execute()
+                    .body();
+
+            JSONObject jsonResponse = JSONUtil.parseObj(response);
+
+            if (jsonResponse.containsKey("balance")) {
+                long balanceSun = jsonResponse.getLong("balance");
+                // 转换为 TRX: 1 TRX = 1,000,000 SUN
+                return BigDecimal.valueOf(balanceSun)
+                        .divide(BigDecimal.valueOf(1_000_000L), 6, BigDecimal.ROUND_DOWN);
+            } else {
+                // 账户不存在或没有余额
+                return BigDecimal.ZERO;
+            }
+
+        } catch (Exception e) {
+            throw new RuntimeException("查询TRX余额失败: " + e.getMessage(), e);
+        }
+    }
+
+
+
+    /**
+     * 查询可读代币余额
+     * @param targetAddress 目标地址
+     * @param contractAddress 合约地址
+     * @param decimals 代币精度
+     * @return 转换后的金额
+     */
+    public static BigDecimal getTokenBalance(String targetAddress, String contractAddress, int decimals) {
+        BigInteger balanceWei = getTokenBalanceWei(targetAddress, contractAddress);
+        BigDecimal divisor = BigDecimal.TEN.pow(decimals);
+        return new BigDecimal(balanceWei).divide(divisor, decimals, RoundingMode.DOWN);
+    }
+
+    /**
+     * 通过地址和合约查询代币余额
+     * @param ownerAddress    持有者地址
+     * @param contractAddress 代币合约地址
+     * @return 未转换的代币余额(wei)
+     */
+    public static BigInteger getTokenBalanceWei(String ownerAddress, String contractAddress) {
+        String url = TRONGRID_BASE_URL + "/wallet/triggerconstantcontract";
+
+        // 编码地址参数
+        String parameter = TronHexAndBase58Util.base58ToHex(ownerAddress);
+
+        JSONObject params = new JSONObject();
+        params.set("owner_address", ownerAddress);
+        params.set("contract_address", contractAddress);
+        params.set("function_selector", "balanceOf(address)");
+        params.set("parameter", parameter);
+        params.set("visible", true);
+
+        try {
+            String response = HttpRequest.post(url)
+                    .header("Content-Type", "application/json")
+                    .timeout(10000)
+                    .body(params.toString())
+                    .execute()
+                    .body();
+
+            JSONObject jsonResponse = JSONUtil.parseObj(response);
+
+            if (jsonResponse.containsKey("constant_result")) {
+                String hexBalance = jsonResponse.getJSONArray("constant_result").getStr(0);
+                if (hexBalance != null && !hexBalance.isEmpty()) {
+                    try {
+                        return new BigInteger(hexBalance,16);//解析16进制
+                    } catch (NumberFormatException e) {
+                        throw new RuntimeException("解析代币余额失败: " + hexBalance, e);
+                    }
+                }
+            }
+            return BigInteger.ZERO;
+
+        } catch (Exception e) {
+            throw new RuntimeException("查询代币余额失败: " + e.getMessage(), e);
+        }
+    }
+
+
+
+
+    /**
+     * 通过交易哈希查询交易状态(基于实际的事件日志数据)
+     */
+    public static TransactionInfoDTO getTransactionStatus(String txHash) {
+        // 获取交易收据(包含事件日志)
+        JSONObject txReceipt = getTransactionReceipt(txHash);
+
+        TransactionInfoDTO dto = new TransactionInfoDTO();
+
+        // 基础信息
+        dto.setHash(txHash);
+        dto.setBlockNumber(txReceipt.getLong("blockNumber"));
+        dto.setTimestamp(txReceipt.getLong("blockTimeStamp"));
+
+        // 交易状态
+        String result = txReceipt.getJSONObject("receipt").getStr("result");
+        dto.setStatus("SUCCESS".equals(result) ? "success" : "failed");
+
+        // 手续费信息
+        long feeSun = txReceipt.getLong("fee", 0L);
+        dto.setGasFee(BigDecimal.valueOf(feeSun).divide(BigDecimal.valueOf(1_000_000L), 6, RoundingMode.DOWN));
+
+        // 解析事件日志
+        JSONArray logs = txReceipt.getJSONArray("log");
+        if (!logs.isEmpty()) {
+            JSONObject log = logs.getJSONObject(0);
+            JSONArray topics = log.getJSONArray("topics");
+            String data = log.getStr("data");
+
+            // 解析 Transfer 事件
+            if (topics.size() >= 3) {
+                // 解析 from 地址(topics[1])
+                String fromHex = topics.getStr(1).substring(24); // 取后40个字符
+                dto.setFromAddress(TronHexAndBase58Util.hexToBase58(fromHex));
+
+                // 解析 to 地址(topics[2])
+                String toHex = topics.getStr(2).substring(24); // 取后40个字符
+                dto.setToAddress(TronHexAndBase58Util.hexToBase58(toHex));
+
+                // 解析金额(data 字段)
+                if (data != null && !data.isEmpty()) {
+                    BigInteger valueWei = new BigInteger(data.substring(24), 16); // 取后64个字符
+
+                    // 代币信息
+                    String contractAddress = txReceipt.getStr("contract_address");
+                    dto.setContractAddress(contractAddress);
+
+
+                    // todo 根据合约地址确定代币符号和精度
+                    dto.setDecimals(6);
+                    // 金额处理
+                    BigDecimal amount = new BigDecimal(valueWei).divide(BigDecimal.TEN.pow(dto.getDecimals()), dto.getDecimals(), RoundingMode.DOWN);
+                    dto.setAmount(amount);
+                }
+            }
+        }
+
+        return dto;
+    }
+
+    /**
+     * 查询交易事件
+     */
+    private static JSONObject getTransactionEvents(String txHash) {
+        String url = TRONGRID_BASE_URL + "/v1/transactions/" + txHash + "/events";
+
+        try {
+            String response = HttpRequest.get(url)
+                    .header("accept", "application/json")
+                    .timeout(10000)
+                    .execute()
+                    .body();
+
+            return JSONUtil.parseObj(response);
+
+        } catch (Exception e) {
+            throw new RuntimeException("查询交易事件失败: " + e.getMessage(), e);
+        }
+    }
+    /**
+     * 3.1 查询交易基本信息
+     */
+    private static JSONObject getTransactionInfo(String txHash) {
+        String url = TRONGRID_BASE_URL + "/wallet/gettransactionbyid";
+
+        JSONObject params = new JSONObject();
+        params.put("value", txHash);
+        params.put("visible", true);
+
+        try {
+            String response = HttpRequest.post(url)
+                    .header("Content-Type", "application/json")
+                    .timeout(10000)
+                    .body(params.toString())
+                    .execute()
+                    .body();
+
+            return JSONUtil.parseObj(response);
+
+        } catch (Exception e) {
+            throw new RuntimeException("查询交易信息失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 3.2 查询交易收据(包含执行结果)
+     */
+    private static JSONObject getTransactionReceipt(String txHash) {
+        String url = TRONGRID_BASE_URL + "/wallet/gettransactioninfobyid";
+
+        JSONObject params = new JSONObject();
+        params.set("value", txHash);
+        params.set("visible", true);
+
+        try {
+            String response = HttpRequest.post(url)
+                    .header("Content-Type", "application/json")
+                    .timeout(10000)
+                    .body(params.toString())
+                    .execute()
+                    .body();
+
+            return JSONUtil.parseObj(response);
+
+        } catch (Exception e) {
+            // 交易收据可能不存在,返回空对象
+            return new JSONObject();
+        }
+    }
+
+//    /**
+//     * 3.3 分析交易状态
+//     */
+//    private static JSONObject analyzeTransactionStatus(JSONObject txInfo, JSONObject txReceipt) {
+//        JSONObject status = new JSONObject();
+//        status.set("tx_hash", txInfo.getStr("txID"));
+//
+//        // 判断交易是否确认
+//        if (txInfo.containsKey("ret")) {
+//            JSONArray retArray = txInfo.getJSONArray("ret");
+//            if (retArray.size() > 0) {
+//                JSONObject ret = retArray.getJSONObject(0);
+//                String retCode = ret.getStr("ret");
+//                status.set("confirmed", !"FAILED".equals(retCode));
+//                status.set("ret_code", retCode);
+//            }
+//        }
+//
+//        // 检查交易收据(更详细的状态)
+//        if (!txReceipt.isEmpty() && txReceipt.containsKey("result")) {
+//            String result = txReceipt.getStr("result");
+//            status.set("execution_result", result);
+//            status.set("success", "SUCCESS".equals(result));
+//
+//            // 能量消耗
+//            if (txReceipt.containsKey("energy_usage")) {
+//                status.set("energy_used", txReceipt.getLong("energy_usage"));
+//            }
+//            if (txReceipt.containsKey("energy_penalty")) {
+//                status.set("energy_penalty", txReceipt.getLong("energy_penalty"));
+//            }
+//
+//            // 合约结果(代币交易)
+//            if (txReceipt.containsKey("contract_result")) {
+//                status.set("contract_result", txReceipt.getJSONArray("contract_result"));
+//            }
+//        }
+//
+//        // 交易基本信息
+//        if (txInfo.containsKey("raw_data")) {
+//            JSONObject rawData = txInfo.getJSONObject("raw_data");
+//            status.set("timestamp", rawData.getLong("timestamp"));
+//            status.set("expiration", rawData.getLong("expiration"));
+//
+//            // 交易类型
+//            if (rawData.containsKey("contract")) {
+//                JSONArray contracts = rawData.getJSONArray("contract");
+//                if (contracts.size() > 0) {
+//                    JSONObject contract = contracts.getJSONObject(0);
+//                    status.set("contract_type", contract.getStr("type"));
+//                }
+//            }
+//        }
+//
+//        return status;
+//    }
+
+    /**
+     * 4. 通过地址查询账户能量余额
+     *
+     * @param address TRON地址
+     * @return 能量信息
+     */
+    public static JSONObject getAccountEnergy(String address) {
+        String url = TRONGRID_BASE_URL + "/wallet/getaccountresource";
+
+        JSONObject params = new JSONObject();
+        params.put("address", address);
+        params.put("visible", true);
+
+        try {
+            String response = HttpRequest.post(url)
+                    .header("Content-Type", "application/json")
+                    .timeout(10000)
+                    .body(params.toString())
+                    .execute()
+                    .body();
+
+            JSONObject resourceData = JSONUtil.parseObj(response);
+            return parseEnergyInfo(address, resourceData);
+
+        } catch (Exception e) {
+            throw new RuntimeException("查询账户能量失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 4.1 解析能量信息
+     */
+    private static JSONObject parseEnergyInfo(String address, JSONObject resourceData) {
+        JSONObject energyInfo = new JSONObject();
+        energyInfo.set("address", address);
+
+        // 能量信息
+        if (resourceData.containsKey("EnergyUsed")) {
+            energyInfo.set("energy_used", resourceData.getLong("EnergyUsed"));
+        }
+        if (resourceData.containsKey("EnergyLimit")) {
+            energyInfo.set("energy_limit", resourceData.getLong("EnergyLimit"));
+        }
+
+        // 计算剩余能量
+        long energyUsed = resourceData.getLong("EnergyUsed", 0L);
+        long energyLimit = resourceData.getLong("EnergyLimit", 0L);
+        long energyRemaining = Math.max(0, energyLimit - energyUsed);
+        energyInfo.set("energy_remaining", energyRemaining);
+
+        // 带宽信息
+        if (resourceData.containsKey("free_net_used")) {
+            energyInfo.set("free_bandwidth_used", resourceData.getLong("free_net_used"));
+        }
+        if (resourceData.containsKey("free_net_limit")) {
+            energyInfo.set("free_bandwidth_limit", resourceData.getLong("free_net_limit"));
+        }
+
+        // 计算剩余带宽
+        long bandwidthUsed = resourceData.getLong("free_net_used", 0L);
+        long bandwidthLimit = resourceData.getLong("free_net_limit", 0L);
+        long bandwidthRemaining = Math.max(0, bandwidthLimit - bandwidthUsed);
+        energyInfo.set("free_bandwidth_remaining", bandwidthRemaining);
+
+        return energyInfo;
+    }
+
+    // ========== 工具方法 ==========
+
+
+//
+//    /**
+//     * 解析代币余额
+//     */
+//    private static BigDecimal parseTokenBalance(JSONObject response, int decimals) {
+//        if (response.containsKey("constant_result")) {
+//            String hexBalance = response.getJSONArray("constant_result").getStr(0);
+//            if (hexBalance != null && !hexBalance.isEmpty()) {
+//                try {
+//                    BigInteger balanceWei = new BigInteger(hexBalance, 16);
+//                    BigDecimal divisor = BigDecimal.TEN.pow(decimals);
+//                    return new BigDecimal(balanceWei).divide(divisor, decimals, BigDecimal.ROUND_DOWN);
+//                } catch (NumberFormatException e) {
+//                    throw new RuntimeException("解析代币余额失败: " + hexBalance, e);
+//                }
+//            }
+//        }
+//        return BigDecimal.ZERO;
+//    }
+//
+//    /**
+//     * 添加请求头(包含API Key)
+//     */
+//    private static HttpRequest addHeaders(HttpRequest request) {
+//        request.header("Content-Type", "application/json");
+//        if (apiKey != null && !apiKey.isEmpty()) {
+//            request.header("TRON-PRO-API-KEY", apiKey);
+//        }
+//        return request;
+//    }
+}

+ 18 - 0
blockchain-transfer/src/main/java/com/table/transfer/util/aUtil.java

@@ -0,0 +1,18 @@
+package com.table.transfer.util;
+
+public class aUtil {
+    public static String assetCode2Chain(String assetCode) throws Exception {
+        if(assetCode.contains("-")){
+            return assetCode.substring(assetCode.indexOf("-")+1);
+        }
+        throw new Exception("不正确的assetCode");
+    }
+    public static String assetCode2Coin(String assetCode) throws Exception {
+        if(assetCode.contains("-")){
+            return assetCode.substring(0,assetCode.indexOf("-")-1);
+        }
+        throw new Exception("不正确的assetCode");
+    }
+
+
+}

+ 190 - 0
blockchain-transfer/src/main/java/com/table/transfer/util/encryp/RSAEncryptionUtil.java

@@ -0,0 +1,190 @@
+package com.table.transfer.util.encryp;
+
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Cipher;
+import java.security.*;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+@Slf4j
+
+public class RSAEncryptionUtil {
+
+    public static void a() {
+        try {
+            System.out.println("开始初始化 Bouncy Castle...");
+            System.out.println("Java 版本: " + System.getProperty("java.version"));
+            System.out.println("Java Home: " + System.getProperty("java.home"));
+
+            // 列出所有已注册的安全提供者
+            Provider[] providers = Security.getProviders();
+            System.out.println("当前安全提供者:");
+            for (Provider provider : providers) {
+                System.out.println(" - " + provider.getName() + " v" + provider.getVersion());
+            }
+
+            // 检查BC是否已存在 - 这里修改:不移除,直接使用现有的
+            Provider existingBC = Security.getProvider("BC");
+            if (existingBC != null) {
+                System.out.println("发现已存在的 BC 提供者: " + existingBC.getInfo());
+                // 不移除,直接使用现有的BC提供者
+                System.out.println("使用现有的 BC 提供者");
+            } else {
+                // 添加新的BC提供者
+                BouncyCastleProvider newProvider = new BouncyCastleProvider();
+                Security.addProvider(newProvider);
+                System.out.println("成功添加 Bouncy Castle 提供者: " + newProvider.getInfo());
+            }
+
+            // 验证BC提供者
+            Provider verifiedBC = Security.getProvider("BC");
+            if (verifiedBC != null) {
+                System.out.println("BC 提供者验证成功: " + verifiedBC.getName());
+            } else {
+                System.err.println("BC 提供者验证失败!");
+            }
+
+            System.out.println("=== 开始加密测试 ===");
+
+            // 测试加密 - 添加更详细的错误处理
+            try {
+                String testText = "尝试进行加密";
+                System.out.println("加密原文: " + testText);
+
+                String encrypted = encrypt(testText);
+                System.out.println("加密成功,结果: " + encrypted);
+                log.info("加密后: " + encrypted);
+
+                System.out.println("=== 开始解密测试 ===");
+                String decrypted = decrypt(encrypted);
+                System.out.println("解密成功,结果: " + decrypted);
+
+                boolean match = testText.equals(decrypted);
+                System.out.println("加解密验证: " + (match ? "成功" : "失败"));
+
+            } catch (Exception cryptoException) {
+                System.err.println("加解密操作失败:");
+                cryptoException.printStackTrace();
+                log.error("加解密操作失败", cryptoException);
+
+                // 输出更详细的错误信息
+                System.err.println("错误类型: " + cryptoException.getClass().getName());
+                System.err.println("错误信息: " + cryptoException.getMessage());
+
+                // 检查公钥
+                try {
+                    byte[] publicKeyBytes = Base64.getDecoder().decode(pubk);
+                    System.out.println("公钥长度: " + publicKeyBytes.length + " bytes");
+
+                    // 尝试解析公钥
+                    PublicKey publicKey = getPublicKey(publicKeyBytes);
+                    System.out.println("公钥算法: " + publicKey.getAlgorithm());
+                    System.out.println("公钥格式: " + publicKey.getFormat());
+                } catch (Exception keyException) {
+                    System.err.println("公钥解析失败:");
+                    keyException.printStackTrace();
+                }
+            }
+            System.out.println("没有异常");
+        } catch (Exception e) {
+            System.err.println("RSA初始化失败:");
+            e.printStackTrace();
+            log.error("RSA初始化失败", e);
+        }
+
+    }
+
+
+    //todo -d解密
+    private static String PK =
+            "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDAf2lhRMQC8xK529AiMnTXbFHXvDRT5AIhCSHRFqczqhfxIRanuZ0G3+kz0Leqrqom50XuIsi0jcUMoHbB9hgbg1GnuWtHgxTWtP9uxZiGF6++KhWP/XYgpsZYPcRtNc1W/P71wyfX8VHsBDQqbEoi3hTXJyVG4lClnk4mtAUAO+6+PeUsScUC0Pg2kdYKaPtVvlszLEQhFrIkK0YI021DIZEoyAOw87ncbkS77ekxu9dSwvcGo3dW/ggulhQv8LLgshEWBL1oH1VgC6vDjG/skkkIg416GIsRZ5BtQlVzlmxY3hLQGs8CoXhdTa5zTT+rpDy135pBF6k99KJ28/JjAgMBAAECggEABoKf5U2fQOe85qpEkpOBuzNoXHYdp01mtWVQq4/lYzIyel1/er32B9qsC5UHC1h0XC4aXuZqcUu0shRa230CkJ9Hhci6wrvwFMHEU5gaSUU8WnDpvlIbs0eK17snDT3D8UDuduHlx7R+ST5U+JfIb4tyskMDHdKwtY2VJiSr7M7cgFslAhvDhzyeJxMKZ0LGcPiEpLUTi39szgbST8wYxwY65UkAEVLPSNsZjiREhioMFqK3ikMIx1W2QI7la5JO6HVMApMx77CEK12otI/8u3x/myl2L7ABtn5Z1aPkKAUNJYNV7vy20Fcxve8qFv0tPmU6bWZCPoVcG9fjs+jFcQKBgQDr0k8nojol4dLWQfYfkauRH7nt0DzNuO+6pMfcGOs1yg1w6mf8+OCDFkfYnW9XZnzVjFoYd6ln+1QGQBC+DbpVeGFgaB4fWc+/HdXm5a8fASxG5eIPy5yrirsnwzku7q7y4edp0YNgO8/o+wbiuvbf18R9bwOP8MfO9IufpGbQ6QKBgQDQ+BcnnMROIcjKLgKkMsdnPMNRyZg1xrKPgZLQSv/uyeK2N+XsWQMMGUapjC/pKAgNZ1V0oxzHW+wkYeizRFZQPGmAAXT9lmlu5bbryRbNCZ9LyjPiKN7PafArWGi5SaOq2rsrDUz6tstwh9XVj9RGEW7yA4HFaCPeeUpRy4j5awKBgAXau8JVIbJtD8nn609PU34P1pLT156X18O/cYLCT5yPPpQuDZTXLOO0OuZiyi/WSIUYrefrmJYPRD+/eWrudlDG40v8absHhMPO3s+TpixT4CCGdn1D5cjQceMD0riajldTSmZzIBrUgU3cZYhdceGOvr8KcYEyThOSOKEm+LJ5AoGAfnLxR/YTz5ef8b+/t25GvVDGfDI52aifmjj1BverQOqHNm5Cwi7mqiaNXMFIqJShssKpskeVfYlBLo7Uic27URgNWvkGuwCOUh3M/4aSNoYxD2KO3LaQhP/85a28yQoncJalFZsZzshX+IMpwO4krDWWZfn7R+IJenD4qOA5xXsCgYBUTdfq7iZJGwU0YD/MeO/Y8PhIOe2CYo4dp9GyzGncXNxxNULCGBCUC13XWUFxs34thz2OLJQBjiIYNVRsb2ojsqrlQZWgip2PUxSgMaWeDNVu/g2ixNdFcKvROhsKbuZtkoJ6Z/Y8l7JrmAM7f2IMk8fObgKpUaTOrVh2NtnvRw==";
+    private static String pubk =
+            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwH9pYUTEAvMSudvQIjJ012xR17w0U+QCIQkh0RanM6oX8SEWp7mdBt/pM9C3qq6qJudF7iLItI3FDKB2wfYYG4NRp7lrR4MU1rT/bsWYhhevvioVj/12IKbGWD3EbTXNVvz+9cMn1/FR7AQ0KmxKIt4U1yclRuJQpZ5OJrQFADvuvj3lLEnFAtD4NpHWCmj7Vb5bMyxEIRayJCtGCNNtQyGRKMgDsPO53G5Eu+3pMbvXUsL3BqN3Vv4ILpYUL/Cy4LIRFgS9aB9VYAurw4xv7JJJCIONehiLEWeQbUJVc5ZsWN4S0BrPAqF4XU2uc00/q6Q8td+aQRepPfSidvPyYwIDAQAB";
+    private static final String ALGORITHM = "RSA";
+    // 必须显式指定 SHA-256,且强制使用 Bouncy Castle
+
+    // 使用系统默认提供者,不指定BC
+    private static final String TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
+
+    /**
+     * 使用公钥加密数据 - 使用系统默认提供者
+     */
+    public static String encrypt(String plainText) throws Exception {
+        try {
+            System.out.println("开始加密,使用系统默认提供者");
+
+            byte[] publicKeyBytes = Base64.getDecoder().decode(pubk);
+            PublicKey publicKey = getPublicKey(publicKeyBytes);
+
+            // 关键修改:不使用"BC",让系统选择默认提供者
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+
+            byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(encryptedBytes);
+
+        } catch (Exception e) {
+            throw new Exception("加密失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 使用私钥解密数据 - 使用系统默认提供者
+     */
+    public static String decrypt(String encryptedData) throws Exception {
+        try {
+            byte[] privateKeyBytes = Base64.getDecoder().decode(PK);
+            PrivateKey privateKey = getPrivateKey(privateKeyBytes);
+
+            // 关键修改:不使用"BC"
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+            cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+            byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
+            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+            return new String(decryptedBytes, java.nio.charset.StandardCharsets.UTF_8);
+
+        } catch (Exception e) {
+            throw new Exception("解密失败: " + e.getMessage(), e);
+        }
+    }
+
+    private static PublicKey getPublicKey(byte[] keyBytes) throws Exception {
+        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
+        // 关键修改:不使用"BC"
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        return keyFactory.generatePublic(spec);
+    }
+
+    private static PrivateKey getPrivateKey(byte[] keyBytes) throws Exception {
+        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
+        // 关键修改:不使用"BC"
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        return keyFactory.generatePrivate(spec);
+    }
+
+    /**
+     * 生成 RSA 密钥对(2048 位)
+     */
+    public static KeyPair generateKeyPair() throws Exception {
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, "BC");
+        keyPairGenerator.initialize(2048, new SecureRandom());
+        return keyPairGenerator.generateKeyPair();
+    }
+
+    public static String getPublicKeyBase64(KeyPair keyPair) {
+        return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
+    }
+
+    public static String getPrivateKeyBase64(KeyPair keyPair) {
+        return Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
+    }
+
+}

+ 44 - 0
blockchain-transfer/src/main/java/com/table/transfer/util/ether/ChainCoinDetailManager.java

@@ -0,0 +1,44 @@
+package com.table.transfer.util.ether;
+
+import com.table.transfer.module.enpty.ChainCoinDetail;
+import com.table.transfer.module.enpty.TransferRequest;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ChainCoinDetailManager {
+    public static final Map<String, ChainCoinDetail> chainCoinDetailMap = new HashMap<>();
+    private static final String ethNode = "https://billowing-greatest-feather.quiknode.pro/591e36c78f14e843e629d72c07a3fb39c5955541";
+    private static final String bscNode = "https://empty-quiet-sunset.bsc.quiknode.pro/69b72a1ee6e3efe4ad03d31b04a9a1b0c8a6e6d3";
+
+    static {
+        List<ChainCoinDetail> ChainCoinDetailList = new ArrayList<>();
+        ChainCoinDetailList.add(new ChainCoinDetail(1, "ETH", "USDT", "0xdAC17F958D2ee523a2206206994597C13D831ec7", 6, BigInteger.valueOf(800000000L), BigInteger.valueOf(20000000000L), ethNode));
+        ChainCoinDetailList.add(new ChainCoinDetail(1, "ETH", "ETH", null, 18, BigInteger.valueOf(800000000L), BigInteger.valueOf(20000000000L), ethNode));
+        ChainCoinDetailList.add(new ChainCoinDetail(56, "BSC", "USDT", "0x55d398326f99059fF775485246999027B3197955", 18, BigInteger.valueOf(100000000L), BigInteger.valueOf(1000000000L), bscNode));
+        ChainCoinDetailList.add(new ChainCoinDetail(56, "BSC", "BNB", null, 18,  BigInteger.valueOf(100000000L), BigInteger.valueOf(1000000000L), bscNode));
+        ChainCoinDetailList.add(new ChainCoinDetail(56, "BSC", "TRX", "0xCE7de646e7208a4Ef112cb6ed5038FA6cC6b12e3", 6, BigInteger.valueOf(100000000L), BigInteger.valueOf(1000000000L), bscNode));
+        ChainCoinDetailList.add(new ChainCoinDetail(56, "BSC", "ETH", "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", 18, BigInteger.valueOf(100000000L), BigInteger.valueOf(1000000000L), bscNode));
+        ChainCoinDetailList.add(new ChainCoinDetail(56, "BSC", "WBNB", "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", 18, BigInteger.valueOf(100000000L), BigInteger.valueOf(1000000000L), bscNode));
+        ChainCoinDetailList.add(new ChainCoinDetail(56, "BSC", "WBTC", "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", 8, BigInteger.valueOf(100000000L), BigInteger.valueOf(1000000000L), bscNode));
+
+        for (ChainCoinDetail one : ChainCoinDetailList) {
+            chainCoinDetailMap.put(one.getAssetCode(), one);
+        }
+
+    }
+    public static ChainCoinDetail getChainCoinDetail(TransferRequest transferRequest) {
+        return chainCoinDetailMap.get(transferRequest.getAssetCode());
+    }
+
+    public static ChainCoinDetail getChainCoinDetail(String assetCode) {
+        return chainCoinDetailMap.get(assetCode);
+    }
+    public static int getDecimal(TransferRequest transferRequest) {
+       return chainCoinDetailMap.get(transferRequest.getAssetCode()).getDecimal();
+    }
+
+}

+ 47 - 0
blockchain-transfer/src/main/java/com/table/transfer/util/ether/Web3jManager.java

@@ -0,0 +1,47 @@
+package com.table.transfer.util.ether;
+
+import jakarta.annotation.PostConstruct;
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.http.HttpService;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+public class Web3jManager {
+    private static final String ethNode = "https://billowing-greatest-feather.quiknode.pro/591e36c78f14e843e629d72c07a3fb39c5955541";
+    private static final String bscNode = "https://empty-quiet-sunset.bsc.quiknode.pro/69b72a1ee6e3efe4ad03d31b04a9a1b0c8a6e6d3";
+
+    public static final Map<String, Web3j> web3jClients = new ConcurrentHashMap<>();
+
+     static {
+        // 假设你有多个链的节点配置
+        registerWeb3j("ETH", ethNode);
+        registerWeb3j("BSC", bscNode);
+    }
+    public static void registerWeb3j(String chain, String nodeUrl) {
+        OkHttpClient httpClient = new OkHttpClient.Builder()
+                .connectTimeout(10, TimeUnit.SECONDS)
+                .readTimeout(30, TimeUnit.SECONDS)
+                .writeTimeout(30, TimeUnit.SECONDS)
+                .connectionPool(new ConnectionPool(10, 60, TimeUnit.SECONDS))
+                .retryOnConnectionFailure(true) // 对 GET 请求自动重试
+                .build();
+
+        HttpService httpService = new HttpService(nodeUrl, httpClient, false);
+        Web3j web3j = Web3j.build(httpService);
+        web3jClients.put(chain, web3j);
+    }
+
+    public static Web3j getWeb3j(String chain) {
+        Web3j web3j = web3jClients.get(chain);
+        if (web3j == null) {
+            throw new IllegalArgumentException("不支持的链: " + chain);
+        }
+        return web3j;
+    }
+
+}

+ 90 - 0
blockchain-transfer/src/main/java/com/table/transfer/util/tron/TronHexAndBase58Util.java

@@ -0,0 +1,90 @@
+package com.table.transfer.util.tron;
+
+import cn.hutool.core.codec.Base58;
+import cn.hutool.core.util.HexUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+
+import java.util.Arrays;
+
+/**
+ * TRON 地址转换工具(纯 Hutool 版本)
+ */
+public class TronHexAndBase58Util {
+    
+    /**
+     * Hex 地址转 Base58 地址
+     */
+    public static String hexToBase58(String hexAddress) {
+        try {
+            // 移除可能存在的 0x 前缀
+            if (hexAddress.startsWith("0x")) {
+                hexAddress = hexAddress.substring(2);
+            }
+            
+            if (hexAddress.length() != 40) {
+                throw new IllegalArgumentException("无效的地址长度: " + hexAddress.length());
+            }
+            
+            // 添加 TRON 地址前缀 0x41
+            String fullHex = "41" + hexAddress;
+            
+            // 使用 Hutool 将 Hex 转换为字节数组
+            byte[] addressBytes = HexUtil.decodeHex(fullHex);
+            
+            // 计算校验和
+            byte[] checksum = calculateChecksum(addressBytes);
+            
+            // 组合地址字节和校验和
+            byte[] fullBytes = new byte[addressBytes.length + checksum.length];
+            System.arraycopy(addressBytes, 0, fullBytes, 0, addressBytes.length);
+            System.arraycopy(checksum, 0, fullBytes, addressBytes.length, checksum.length);
+            
+            // 使用 Hutool 的 Base58 编码
+            return Base58.encode(fullBytes);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Hex 转 Base58 失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * Base58 地址转 Hex 地址
+     */
+    public static String base58ToHex(String base58Address) {
+        try {
+            // 使用 Hutool 的 Base58 解码
+            byte[] decoded = Base58.decode(base58Address);
+            
+            // 提取地址部分(前21字节)
+            byte[] addressBytes = Arrays.copyOfRange(decoded, 0, 21);
+            
+            // 使用 Hutool 将字节数组转换为 Hex
+            String hexAddress = HexUtil.encodeHexStr(addressBytes);
+            
+            if (hexAddress.startsWith("41") && hexAddress.length() == 42) {
+                return "000000000000000000000000" + hexAddress.substring(2);
+            } else {
+                throw new IllegalArgumentException("无效的 TRON 地址格式: " + hexAddress);
+            }
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Base58 转 Hex 失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 计算校验和
+     */
+    private static byte[] calculateChecksum(byte[] data) {
+        byte[] hash1 = DigestUtil.sha256(data);
+        byte[] hash2 = DigestUtil.sha256(hash1);
+        return Arrays.copyOfRange(hash2, 0, 4);
+    }
+    
+    /**
+     * 验证 TRON 地址格式
+     */
+    public static boolean isValidTronAddress(String address) {
+        return address != null && address.length() == 34 && address.startsWith("T");
+    }
+}

+ 41 - 0
blockchain-transfer/src/main/resources/application-dev.yaml

@@ -0,0 +1,41 @@
+server:
+  port: 8080
+spring:
+  application:
+    name: blockchain-transfer-service
+  jackson:
+    time-zone: GMT+8
+    date-format: yyyy-MM-dd HH:mm:ss
+  rabbitmq:
+    host: localhost
+    port: 5672
+    username: guest
+    password: guest
+    virtual-host: /adae
+    connection-timeout: 10000  # 增加超时时间
+  #mysql
+  datasource:
+    url: jdbc:mysql://156.233.229.79:3306/echo2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+    username: echo2
+    password: 6YdejnWXdFdTRwsB
+    driver-class-name: com.mysql.cj.jdbc.Driver
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 开发时开启SQL日志
+  global-config:
+    db-config:
+      id-type: auto
+      logic-delete-field: deleted  # 逻辑删除字段名
+      logic-delete-value: 1
+      logic-not-delete-value: 0
+  mapper-locations: classpath*:/mapper/**/*.xml
+logging:
+  level:
+    org.springframework.amqp: DEBUG  # 添加 RabbitMQ 调试日志
+    com.table.transfer: INFO
+    com.example.blockchain: INFO
+  file:
+    name: logs/transfer.log
+  pattern:
+    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

+ 48 - 0
blockchain-transfer/src/main/resources/application-local.yaml

@@ -0,0 +1,48 @@
+# application.yml
+server:
+  port: 8080
+
+
+
+spring:
+  application:
+    name: blockchain-transfer-service
+  jackson:
+    time-zone: GMT+8
+    date-format: yyyy-MM-dd HH:mm:ss
+  rabbitmq:
+    host: localhost
+    port: 5672
+    username: guest
+    password: guest
+    virtual-host: /adae
+    connection-timeout: 10000  # 增加超时时间
+  #mysql
+  datasource:
+    url: jdbc:mysql://156.233.229.79:3306/echo2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+    username: echo2
+    password: 6YdejnWXdFdTRwsB
+    driver-class-name: com.mysql.cj.jdbc.Driver
+
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 开发时开启SQL日志
+  global-config:
+    db-config:
+      id-type: auto
+      logic-delete-field: del_flag  # 逻辑删除字段名
+      logic-delete-value: 1
+      logic-not-delete-value: 0
+  mapper-locations: classpath*:/mapper/**/*.xml
+
+logging:
+  level:
+    org.springframework.amqp: DEBUG  # 添加 RabbitMQ 调试日志
+    com.table.transfer: INFO
+    com.example.blockchain: INFO
+  file:
+    name: logs/transfer.log
+  pattern:
+    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
+

+ 41 - 0
blockchain-transfer/src/main/resources/application-prod.yaml

@@ -0,0 +1,41 @@
+server:
+  port: 8888
+spring:
+  application:
+    name: blockchain-transfer-service
+  jackson:
+    time-zone: GMT+8
+    date-format: yyyy-MM-dd HH:mm:ss
+  rabbitmq:
+    host: 127.0.0.1
+    port: 5672
+    username: blockchain
+    password: bl@nkchaine69139fa
+    virtual-host: /adae
+    connection-timeout: 10000  # 增加超时时间
+  #mysql
+  datasource:
+    url: jdbc:mysql://t6w2dyvggz5fk.aliyun-ap-southeast-1.oceanbase.cloud:3306/adae?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+    username: adae
+    password: q8s:%~[(m7lStxC;@_bX31~7l7xj)z
+    driver-class-name: com.mysql.cj.jdbc.Driver
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 开发时开启SQL日志
+  global-config:
+    db-config:
+      id-type: auto
+      logic-delete-field: deleted  # 逻辑删除字段名
+      logic-delete-value: 1
+      logic-not-delete-value: 0
+  mapper-locations: classpath*:/mapper/**/*.xml
+logging:
+  level:
+    org.springframework.amqp: DEBUG  # 添加 RabbitMQ 调试日志
+    com.table.transfer: INFO
+    com.example.blockchain: INFO
+  file:
+    name: logs/transfer.log
+  pattern:
+    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

+ 4 - 0
blockchain-transfer/src/main/resources/application.yaml

@@ -0,0 +1,4 @@
+spring:
+  profiles:
+    active: @profile.active@
+

+ 55 - 0
blockchain-transfer/src/test/java/BlockchainServiceTest.java

@@ -0,0 +1,55 @@
+import cn.hutool.json.JSONUtil;
+import com.table.transfer.module.enpty.TransactionInfoDTO;
+import com.table.transfer.module.enpty.TransferRequest;
+import com.table.transfer.module.service.EthereumService;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public class BlockchainServiceTest {
+    private static final EthereumService ethereumService= new EthereumService();
+
+    //   SENDADDRESSMAP.put("ETH","0x6F18844e79BeefF5f2D1eE30508aB695A829C12e");
+//        SENDADDRESSMAP.put("tron","TDFtN7nyfweR8WeUraZM7WuCrtUTruD4Er");
+//        SENDADDRESSMAP.put("BSC","0xcBea8F7907078B07B0f491D1B65Ea92905E57827");
+
+
+    public static void main(String[] args) throws Exception {
+//        Web3jManager.init();
+        TransferRequest bscTransferRequest = new TransferRequest();
+        bscTransferRequest.setToAddress("0x4472E7346a38AfB457562895c4Aa974C2A516a84");
+        bscTransferRequest.setFromAddress("0xe78d1f011c52f15012a7f964072750e7b8d7b80a");
+        bscTransferRequest.setPrivateKey("7f79238fc14b539fc6d7d3697db7fc4c2578f03307547b8625ff30f6d48da433");
+        bscTransferRequest.setAmount("0.0000097311403");
+        bscTransferRequest.setContractAddress("0x55d398326f99059ff775485246999027b3197955");
+        bscTransferRequest.setChain("BSC");
+        bscTransferRequest.setCoin("BNB");
+        //獲取nonce
+        BigInteger bscNonce = ethereumService.getNonce(bscTransferRequest.getAssetCode(), bscTransferRequest.getFromAddress());
+        System.out.println("bsc链获取nonce" + bscNonce);
+        //获取gasLimit和SafeGasPrice
+        BigInteger bscSafeGasPrice = ethereumService.getSafeGasPrice(bscTransferRequest);
+        System.out.println("bsc链获取GasPrice" + bscSafeGasPrice);
+        BigInteger bscGasLimit = ethereumService.estimateGasSafely(bscTransferRequest);
+        System.out.println("bsc链获取bscGasLimit" + bscGasLimit);
+        //获取代币和主币余额
+        BigDecimal tokenBalance=ethereumService.getTokenBalance(bscTransferRequest.getAssetCode(),bscTransferRequest.getContractAddress(),bscTransferRequest.getFromAddress());
+        BigDecimal balance = ethereumService.getBalance(bscTransferRequest.getChain(), bscTransferRequest.getFromAddress());
+        System.out.println("代币余额:"+tokenBalance);
+        System.out.println("主币余额:"+balance);
+
+        Long lastBlock = ethereumService.getLastBlock(bscTransferRequest.getChain());
+        System.out.println("获取最"+bscTransferRequest.getChain()+"新区块:"+lastBlock);
+
+
+        TransactionInfoDTO transactionInfoByHash = ethereumService.getTransactionInfoByHash(bscTransferRequest.getAssetCode(), "0xda1fbf2fe5f5afbe2c7fde92bce9eb2d1baa6e6b3a5ca430845b78ce879422a3");
+        System.out.println(bscTransferRequest.getChain()+"链根据hash查询交易结果:"+JSONUtil.toJsonStr(transactionInfoByHash));
+
+
+        ethereumService.transfer(bscTransferRequest);
+        BigInteger ethNonce = ethereumService.getNonce("USDT-ETH", "0x6F18844e79BeefF5f2D1eE30508aB695A829C12e");
+        System.out.println("eth链获取nonce" + ethNonce);
+
+    }
+
+}