使用solanaJ 1.20.2发送Solana链USDT生成签名但链上无记录排查
问题描述
使用solanaJ 1.20.2版本在Solana主网发送USDT,签名可正常生成且无报错,但链上无法查询到该交易记录。
依赖配置
<dependency> <groupId>com.mmorrell</groupId> <artifactId>solanaj</artifactId> <version>1.20.2</version> </dependency>
问题代码
public static String transferUsdt(CryptoAccount fromAccount, String toAddress, long amount) { RpcClient rpcClient = new RpcClient(Cluster.MAINNET); String senderPrivateKeyBase58 = fromAccount.getPrivateKey(); Account senderAccount = new Account(Base58.decode(senderPrivateKeyBase58)); try { PublicKey usdtPublicKey = PublicKey.valueOf("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); PublicKey tokenAccountFrom = rpcClient.getApi().getTokenAccountsByOwner(senderAccount.getPublicKey(), usdtPublicKey); PublicKey tokenAccountTo = rpcClient.getApi().getTokenAccountsByOwner(new PublicKey(toAddress), usdtPublicKey); Transaction transaction = new Transaction(); transaction.addInstruction( ComputeBudgetProgram.setComputeUnitPrice(80000) ); transaction.addInstruction( TokenProgram.transfer( tokenAccountFrom, tokenAccountTo, amount, senderAccount.getPublicKey() ) ); String signature = rpcClient.getApi().sendTransaction(transaction, senderAccount); ConfirmedTransaction confirmedTransaction = rpcClient.getApi().getTransaction(signature); long start = System.currentTimeMillis(); while (Objects.isNull(confirmedTransaction) && (System.currentTimeMillis() - start) < 120000) { Thread.sleep(5000); SignatureStatuses signatureStatuses = rpcClient.getApi().getSignatureStatuses(List.of(signature), true); if (Objects.isNull(signatureStatuses)) { System.out.println("transaction fail"); break; } confirmedTransaction = rpcClient.getApi().getTransaction(signature); } if (Objects.nonNull(confirmedTransaction) && Objects.isNull(confirmedTransaction.getMeta().getErr())) { return signature; } } catch (RpcException e) { } catch (InterruptedException e) { throw new RuntimeException(e); } return null; }
排查出的问题点及修复方案
1. 吞掉RpcException,丢失关键错误信息
代码中catch (RpcException e)块为空,没有任何日志输出。sendTransaction返回签名不代表交易已成功提交到链上,可能只是客户端生成了签名但提交过程中出现RPC错误(比如节点拒绝、手续费异常、账户非法等),但异常被吞掉导致无法定位问题。
修复:
在RpcException捕获块中添加日志输出,打印异常详情:
catch (RpcException e) { System.err.println("RPC调用异常: " + e.getMessage()); e.printStackTrace(); }
2. 代币账户获取逻辑错误
rpcClient.getApi().getTokenAccountsByOwner返回的是符合条件的代币账户列表,而非单个PublicKey。当前代码直接将返回值赋值给PublicKey变量,会导致类型不匹配,交易构造失败,自然无法上链。
修复:
正确处理代币账户列表,选择可用的发送方账户;若接收方无对应USDT代币账户,需先创建关联账户:
// 获取发送方USDT代币账户列表 List<TokenAccount> fromTokenAccounts = rpcClient.getApi().getTokenAccountsByOwner(senderAccount.getPublicKey(), usdtPublicKey); if (fromTokenAccounts.isEmpty()) { System.err.println("发送方无USDT代币账户"); return null; } // 取第一个可用的代币账户(可根据余额筛选) PublicKey tokenAccountFrom = fromTokenAccounts.get(0).getPublicKey(); // 处理接收方代币账户 PublicKey toPublicKey = new PublicKey(toAddress); List<TokenAccount> toTokenAccounts = rpcClient.getApi().getTokenAccountsByOwner(toPublicKey, usdtPublicKey); PublicKey tokenAccountTo; if (toTokenAccounts.isEmpty()) { // 接收方无USDT账户,创建关联账户 tokenAccountTo = TokenProgram.createAssociatedTokenAccount( senderAccount.getPublicKey(), toPublicKey, usdtPublicKey ); transaction.addInstruction(TokenProgram.createAssociatedTokenAccountInstruction( senderAccount.getPublicKey(), tokenAccountTo, toPublicKey, usdtPublicKey )); } else { tokenAccountTo = toTokenAccounts.get(0).getPublicKey(); }
3. 交易未设置区块哈希和付费方
Solana交易必须包含最近的区块哈希才能被网络验证,当前代码创建Transaction后未设置该参数,会导致交易提交后被节点直接拒绝,无法进入打包流程。
修复:
构造交易后,获取并设置最近区块哈希及付费方:
Transaction transaction = new Transaction(); // 获取并设置最近区块哈希 RecentBlockhash recentBlockhash = rpcClient.getApi().getRecentBlockhash(); transaction.setRecentBlockhash(recentBlockhash.getBlockhash()); // 设置交易付费方 transaction.setFeePayer(senderAccount.getPublicKey());
4. 签名状态检查逻辑不准确
getSignatureStatuses即使签名无效也会返回包含错误信息的对象,不会返回null。当前判断逻辑无法正确识别交易错误状态。
修复:
检查返回的签名状态中的错误信息:
SignatureStatuses signatureStatuses = rpcClient.getApi().getSignatureStatuses(List.of(signature), true); if (signatureStatuses != null && !signatureStatuses.getValue().isEmpty()) { SignatureStatus status = signatureStatuses.getValue().get(0); if (status.getErr() != null) { System.err.println("交易错误: " + status.getErr()); break; } }
5. 计算预算单价过高(非核心问题,但需优化)
ComputeBudgetProgram.setComputeUnitPrice(80000)设置的手续费单价远高于主网平均水平(通常100-500 lamports),虽不会直接导致交易不上链,但会造成不必要的手续费支出,建议调整为合理数值。
修复后的完整代码示例
public static String transferUsdt(CryptoAccount fromAccount, String toAddress, long amount) { RpcClient rpcClient = new RpcClient(Cluster.MAINNET); String senderPrivateKeyBase58 = fromAccount.getPrivateKey(); Account senderAccount = new Account(Base58.decode(senderPrivateKeyBase58)); try { PublicKey usdtPublicKey = PublicKey.valueOf("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); // 处理发送方代币账户 List<TokenAccount> fromTokenAccounts = rpcClient.getApi().getTokenAccountsByOwner(senderAccount.getPublicKey(), usdtPublicKey); if (fromTokenAccounts.isEmpty()) { System.err.println("发送方无可用USDT代币账户"); return null; } PublicKey tokenAccountFrom = fromTokenAccounts.get(0).getPublicKey(); // 处理接收方代币账户 PublicKey toPublicKey = new PublicKey(toAddress); List<TokenAccount> toTokenAccounts = rpcClient.getApi().getTokenAccountsByOwner(toPublicKey, usdtPublicKey); PublicKey tokenAccountTo; Transaction transaction = new Transaction(); // 设置区块哈希和付费方 RecentBlockhash recentBlockhash = rpcClient.getApi().getRecentBlockhash(); transaction.setRecentBlockhash(recentBlockhash.getBlockhash()); transaction.setFeePayer(senderAccount.getPublicKey()); if (toTokenAccounts.isEmpty()) { // 创建接收方关联代币账户 tokenAccountTo = TokenProgram.createAssociatedTokenAccount( senderAccount.getPublicKey(), toPublicKey, usdtPublicKey ); transaction.addInstruction(TokenProgram.createAssociatedTokenAccountInstruction( senderAccount.getPublicKey(), tokenAccountTo, toPublicKey, usdtPublicKey )); } else { tokenAccountTo = toTokenAccounts.get(0).getPublicKey(); } // 添加计算预算指令(调整为合理单价) transaction.addInstruction(ComputeBudgetProgram.setComputeUnitPrice(500)); // 添加转账指令 transaction.addInstruction(TokenProgram.transfer( tokenAccountFrom, tokenAccountTo, amount, senderAccount.getPublicKey() )); // 发送交易 String signature = rpcClient.getApi().sendTransaction(transaction, senderAccount); System.out.println("交易签名: " + signature); // 等待交易确认 ConfirmedTransaction confirmedTransaction = null; long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < 120000) { Thread.sleep(5000); confirmedTransaction = rpcClient.getApi().getTransaction(signature); if (confirmedTransaction != null) { if (confirmedTransaction.getMeta().getErr() == null) { System.out.println("交易成功确认"); return signature; } else { System.err.println("交易执行错误: " + confirmedTransaction.getMeta().getErr()); return null; } } // 检查签名状态 SignatureStatuses signatureStatuses = rpcClient.getApi().getSignatureStatuses(List.of(signature), true); if (signatureStatuses != null && !signatureStatuses.getValue().isEmpty()) { SignatureStatus status = signatureStatuses.getValue().get(0); if (status.getErr() != null) { System.err.println("交易状态错误: " + status.getErr()); return null; } } } System.err.println("交易超时未确认"); } catch (RpcException e) { System.err.println("RPC调用异常: " + e.getMessage()); e.printStackTrace(); } catch (InterruptedException e) { System.err.println("线程中断异常"); e.printStackTrace(); } return null; }
内容的提问来源于stack exchange,提问作者Chao Wei

