parent
9c9fe6075c
commit
4fb4f8915a
46 changed files with 24 additions and 3246 deletions
@ -1,53 +0,0 @@ |
||||
package com.blockchain.server.btc.common.util; |
||||
|
||||
import com.blockchain.server.btc.common.constants.BtcAddressConstans; |
||||
import com.blockchain.server.btc.service.BtcWalletService; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.Set; |
||||
|
||||
@Component |
||||
public class BtcAddressSetRedisUtils { |
||||
@Autowired |
||||
private RedisTemplate redisTemplate; |
||||
|
||||
@Autowired |
||||
private BtcWalletService btcWalletService; |
||||
|
||||
/** |
||||
* 保存地址集合到redis |
||||
* |
||||
* @param address 新增地址 |
||||
*/ |
||||
public void insert(String address) { |
||||
redisTemplate.opsForSet().add(BtcAddressConstans.BTC_ADDRESS_KEY, address); |
||||
} |
||||
|
||||
/** |
||||
* 从redis中获取地址集合 |
||||
* |
||||
* @return |
||||
*/ |
||||
public Set<String> get() { |
||||
return redisTemplate.opsForSet().members(BtcAddressConstans.BTC_ADDRESS_KEY); |
||||
} |
||||
|
||||
/** |
||||
* 判断缓存中是否存在该地址 |
||||
* |
||||
* @param addr 地址 |
||||
* @return |
||||
*/ |
||||
public boolean isExistsAddr(String addr) { |
||||
Set<String> addressSet = get(); |
||||
if (addressSet == null || addressSet.size() == 0) { |
||||
addressSet = btcWalletService.getAllWalletAddr(); |
||||
redisTemplate.opsForSet().add(BtcAddressConstans.BTC_ADDRESS_KEY, addressSet.toArray()); |
||||
} |
||||
|
||||
return addressSet.contains(addr); |
||||
} |
||||
|
||||
} |
@ -1,43 +0,0 @@ |
||||
package com.blockchain.server.btc.common.util; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
@Component |
||||
public class BtcBlockRedisUtils { |
||||
@Autowired |
||||
private RedisTemplate redisTemplate; |
||||
|
||||
private static final String ETH_BLOCKCHAIN_OPT_HASH = "btc:blockchain:opt:block"; |
||||
|
||||
/** |
||||
* 根据区块高度,获取爬取的次数 |
||||
* |
||||
* @param blockHeight 区块高度 |
||||
* @return |
||||
*/ |
||||
public int getBlockOptMapCount(int blockHeight) { |
||||
Integer val = (Integer) redisTemplate.opsForHash().get(ETH_BLOCKCHAIN_OPT_HASH, blockHeight); |
||||
return val != null ? val : 0; |
||||
} |
||||
|
||||
/** |
||||
* 爬取次数加1 |
||||
* |
||||
* @param blockHeight 区块高度 |
||||
*/ |
||||
public void incrementBlockOptMapCount(int blockHeight) { |
||||
redisTemplate.opsForHash().increment(ETH_BLOCKCHAIN_OPT_HASH, blockHeight, 1); |
||||
} |
||||
|
||||
/** |
||||
* 移除区块的爬取记录 |
||||
* |
||||
* @param blockHeight 区块高度 |
||||
*/ |
||||
public void delBlockOptMapCount(int blockHeight) { |
||||
redisTemplate.opsForHash().delete(ETH_BLOCKCHAIN_OPT_HASH, blockHeight); |
||||
} |
||||
|
||||
} |
@ -1,35 +0,0 @@ |
||||
package com.blockchain.server.btc.mapper; |
||||
|
||||
import com.blockchain.server.btc.dto.BtcBlockNumberDTO; |
||||
import com.blockchain.server.btc.entity.BtcBlockNumber; |
||||
import org.apache.ibatis.annotations.Param; |
||||
import org.springframework.stereotype.Repository; |
||||
import tk.mybatis.mapper.common.Mapper; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* BtcBlockNumberMapper 数据访问类 |
||||
* |
||||
* @version 1.0 |
||||
* @date 2019-02-16 15:08:16 |
||||
*/ |
||||
@Repository |
||||
public interface BtcBlockNumberMapper extends Mapper<BtcBlockNumber> { |
||||
|
||||
/** |
||||
* 查询已经同步的最大区块号 |
||||
* |
||||
* @return |
||||
*/ |
||||
Integer selectBigest(); |
||||
|
||||
/** |
||||
* 根据状态获取同步区块 |
||||
* |
||||
* @param status 状态 |
||||
* @return |
||||
*/ |
||||
List<BtcBlockNumberDTO> listByStatus(@Param("status") String status); |
||||
|
||||
} |
@ -1,14 +0,0 @@ |
||||
package com.blockchain.server.btc.mapper; |
||||
|
||||
import com.blockchain.server.btc.entity.BtcWalletKey; |
||||
import org.springframework.stereotype.Repository; |
||||
import tk.mybatis.mapper.common.Mapper; |
||||
|
||||
/** |
||||
* BtcWalletKeyMapper 数据访问类 |
||||
* @date 2019-02-16 15:08:16 |
||||
* @version 1.0 |
||||
*/ |
||||
@Repository |
||||
public interface BtcWalletKeyMapper extends Mapper<BtcWalletKey> { |
||||
} |
@ -1,44 +0,0 @@ |
||||
package com.blockchain.server.btc.rpc; |
||||
|
||||
import com.googlecode.jsonrpc4j.JsonRpcHttpClient; |
||||
import org.apache.commons.codec.binary.Base64; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.net.URL; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* @author hugq |
||||
* @date 2019/2/16 15:54 |
||||
*/ |
||||
@Component |
||||
public class BtcOmniRpcClient { |
||||
|
||||
private Logger LOG = LoggerFactory.getLogger(getClass()); |
||||
|
||||
@Value("${btc.rpc.user}") |
||||
public String RPC_USER; //验证用户名
|
||||
@Value("${btc.rpc.password}") |
||||
public String RPC_PASSWORD; //验证密码
|
||||
@Value("${btc.rpc.url}") |
||||
public String RPC_URL; //验证地址
|
||||
|
||||
// 比特币RPC身份认证
|
||||
public JsonRpcHttpClient getClient() { |
||||
JsonRpcHttpClient client = null; |
||||
try { |
||||
String cred = new Base64().encodeToString((RPC_USER + ":" + RPC_PASSWORD).getBytes("UTF-8")); |
||||
Map<String, String> headers = new HashMap<String, String>(1); |
||||
headers.put("Authorization", "Basic " + cred); |
||||
client = new JsonRpcHttpClient(new URL(RPC_URL), headers); |
||||
} catch (Exception e) { |
||||
LOG.info("=== getClient:{} btc client !===", e.getMessage(), e); |
||||
} |
||||
return client; |
||||
} |
||||
|
||||
} |
@ -1,553 +0,0 @@ |
||||
package com.blockchain.server.btc.rpc; |
||||
|
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.server.btc.common.enums.BtcEnums; |
||||
import com.blockchain.server.btc.common.exception.BtcException; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* @author hugq |
||||
* @date 2019/2/16 15:54 |
||||
*/ |
||||
@Component |
||||
public class BtcUtils { |
||||
|
||||
private Logger LOG = LoggerFactory.getLogger(getClass()); |
||||
|
||||
@Autowired |
||||
private BtcOmniRpcClient client; |
||||
|
||||
/** |
||||
* 生成新的钱包地址 |
||||
* |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public String getNewAddress() throws Exception { |
||||
try { |
||||
return client.getClient().invoke("getnewaddress", null, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getNewAddress(String):{} ===", e.getMessage(), e); |
||||
throw new BtcException(BtcEnums.GET_NEW_ADDRESS_ERROR); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取地址的未花费列表 |
||||
* |
||||
* @param address 地址 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public JSONArray listUnspent(String address) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("listunspent", new Object[]{1, Integer.MAX_VALUE, new Object[]{address}}, JSONArray.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.listUnspent(String):{} ===", e.getMessage(), e); |
||||
throw new BtcException(BtcEnums.LIST_UNSPENT_ERROR); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 验证是否有效地址 |
||||
* |
||||
* @param address 地址 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public JSONObject validateAddress(String address) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("validateaddress", new Object[]{address}, JSONObject.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.validateAddress(String):{} ===", e.getMessage(), e); |
||||
throw new BtcException(BtcEnums.ADDRESS_ERROR); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取指定账户/整个钱包BTC余额 |
||||
* |
||||
* @param account 账户名,为空则获取整个钱包余额 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public double getBalance(String account) throws Exception { |
||||
double balance; |
||||
try { |
||||
if (!StringUtils.isEmpty(account)) { |
||||
balance = client.getClient().invoke("getbalance", new Object[]{account}, Double.class); |
||||
} else { |
||||
balance = client.getClient().invoke("getbalance", new Object[]{}, Double.class); |
||||
} |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getBalance(String...):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: account=%s", account)); |
||||
} |
||||
return balance; |
||||
} |
||||
|
||||
/** |
||||
* 从钱包内向指定的地址发送指定数量的比特币,简单交易,钱包内随机指定或创建地址作为发送地址、找零地址 |
||||
* |
||||
* @param toAddress 地址 |
||||
* @param amount 数量 |
||||
* @return 交易ID |
||||
* @throws Exception |
||||
*/ |
||||
public String sendToAddress(String toAddress, String amount) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("sendtoaddress", new Object[]{toAddress, amount}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.sendtoaddress(String, double):{} ===", e.getMessage(), e); |
||||
throw new BtcException(BtcEnums.SENDTOADDRESS_ERROR); |
||||
} |
||||
} |
||||
|
||||
public Object sendMany(String fromaccount, Object target) throws Exception { |
||||
try { |
||||
String txId = client.getClient().invoke("sendmany", new Object[]{fromaccount, target}, Object.class).toString(); |
||||
return txId; |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.signSendToAddress(String, double):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: fromaccount=%s,target=%s", fromaccount, target)); |
||||
} |
||||
} |
||||
|
||||
public Object getRawTransaction(String txId, int verbose) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("getrawtransaction", new Object[]{txId, verbose}, Object.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getRawTransaction(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: txId=%s", txId)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取指定钱包内交易的详细信息 |
||||
* |
||||
* @param txId 交易ID |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public JSONObject getTransaction(String txId) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("gettransaction", new Object[]{txId}, JSONObject.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getTransaction(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: txId=%s", txId)); |
||||
} |
||||
} |
||||
|
||||
public Object setAccount(String address, String account) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("setaccount", new Object[]{address, account}, Object.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.setAccount(String, String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: address=%s,account=%s", address, account)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用返回各地址收到的比特币数量 |
||||
* |
||||
* @param minconf 计入统计结果的交易所需的最小确认数,默认值:1 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public Object listReceivedByAddress(int minconf) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("listreceivedbyaccount", new Object[]{minconf}, Object.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.listReceivedByAddress(int minconf):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: minconf=%s", minconf)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用设置钱包交易支付时采用的每千字节手续费率 |
||||
* |
||||
* @param account 每千字节的手续费 |
||||
* @return true |
||||
* @throws Exception |
||||
*/ |
||||
public boolean setTxFee(double account) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("settxfee", new Object[]{account + ""}, Boolean.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.settxfee(double):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: account=%s", account)); |
||||
} |
||||
} |
||||
|
||||
public Object encryptWallet(String passphrase) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("encryptwallet", new Object[]{passphrase}, Object.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.encryptwallet(String) ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
public Object walletPassphrase(String passphrase, int timeout) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("walletpassphrase", new Object[]{passphrase, timeout}, Object.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.walletpassphrase(String, int):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: passphrase=%s,timeout=%s", passphrase, timeout)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用可以将钱包文件wallet.data安全地拷贝到指定文件或目录 |
||||
* |
||||
* @param destination 备份文件路径名称 |
||||
* @return 成功时返回null |
||||
* @throws Exception |
||||
*/ |
||||
public boolean backupWallet(String destination) throws Exception { |
||||
try { |
||||
Object obj = client.getClient().invoke("backupwallet ", new Object[]{destination}, Object.class); |
||||
return obj == null; |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.backupWallet(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: destination=%s", destination)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用可以导入钱包转储文件(通过dumpwallet调用获得)。该文件中的私钥将添加到节点钱包中。由于加入了新的私钥,该调用可能 需要重新扫描区块链。 |
||||
* |
||||
* @param filename 要导入的钱包转储文件名 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public boolean importWallet(String filename) throws Exception { |
||||
try { |
||||
Object obj = client.getClient().invoke("importwallet ", new Object[]{filename}, Object.class); |
||||
return obj == null; |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.importWallet(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: filename=%s", filename)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 创建一个未签名的裸交易 |
||||
* |
||||
* @param utxo 交易输入数组,成员对象的结构: UTXO的交易id, UTXO的输出序号 |
||||
* @param toAddress 交易输出对象,键为地址 |
||||
* @param amonut 交易输出对象,值为金额 |
||||
* @return 返回生成的未签名交易的序列化字符串 |
||||
* @throws Exception |
||||
*/ |
||||
public String createRawTransaction(Object utxo, String toAddress, double amonut) throws Exception { |
||||
String _amount = String.valueOf(amonut); |
||||
Map<String, String> outputs = new HashMap(); |
||||
outputs.put(toAddress, _amount); |
||||
try { |
||||
return client.getClient().invoke("createrawtransaction", new Object[]{utxo, outputs}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.createRawTransaction(Object):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: inputs=%s,outputs=%s", utxo.toString(), outputs.toString())); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 为裸交易交易增加输入,主要用于指定找零地址 |
||||
* |
||||
* @param hexString 裸交易字符串 |
||||
* @param address 额外的可选项,结构: changeAddress:找零地址,如果不设置则自动从地址池中选择一个新地址 |
||||
* @return 返回更新后的交易信息 |
||||
* @throws Exception |
||||
*/ |
||||
public String fundRawTransaction(String hexString, String address) throws Exception { |
||||
Map<String, String> outputs = new HashMap(); |
||||
outputs.put("changeAddress", address); |
||||
try { |
||||
JSONObject jsonObject = client.getClient().invoke("fundrawtransaction", new Object[]{hexString, outputs}, JSONObject.class); |
||||
return jsonObject.getString("hex"); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.fundRawTransaction(Object):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: hexString=%s,outputs=%s", hexString, outputs.toString())); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 签名交易 |
||||
* |
||||
* @param hexString 交易串 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public String signRawTransaction(String hexString) throws Exception { |
||||
try { |
||||
JSONObject jsonObject = client.getClient().invoke("signrawtransaction", new Object[]{hexString}, JSONObject.class); |
||||
return jsonObject.getString("hex"); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.signrawTransaction(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: hexstring=%s", hexString)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用验证指定交易并将其广播到P2P网络中 |
||||
* |
||||
* @param hexHash 序列化的交易码流 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public String sendRawTransaction(String hexHash) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("sendrawtransaction", new Object[]{hexHash}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.sendRawTransaction(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: hexHash=%s", hexHash)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用将一个序列化的交易字符串解码为JSON对象 |
||||
* |
||||
* @param hex 要解码的裸交易字符串 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public JSONObject decodeRawTransaction(String hex) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("decoderawtransaction", new Object[]{hex}, JSONObject.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.decoderawtransaction(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: hex=%s", hex)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 通过区块高度获取该区块hash |
||||
* |
||||
* @param index |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public String getBlockHash(int index) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("getblockhash", new Object[]{index}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getBlockHash(int):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: index=%s", index)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取最新区块高度 |
||||
* |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public Integer getBlockCount() throws Exception { |
||||
try { |
||||
return client.getClient().invoke("getblockcount", null, Integer.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getBlockHeader():{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取区块内交易id列表集合 |
||||
* |
||||
* @param blockHash 区块hash |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public ArrayList<String> getBlock(String blockHash) throws Exception { |
||||
try { |
||||
JSONObject blockJson = client.getClient().invoke("getblock", new Object[]{blockHash}, JSONObject.class); |
||||
return (ArrayList<String>) blockJson.get("tx"); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getblock(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: blockHash=%s", blockHash)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 通过地址获取私钥 |
||||
* |
||||
* @param address 地址 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public String getPrivateKeyByAddress(String address) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("dumpprivkey", new Object[]{address}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getPrivateKeyByAddress(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: blockHash=%s", address)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 通过区块hash获取区块高度 |
||||
* |
||||
* @param headerHash 区块hash |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public int getBlockHeader(String headerHash) throws Exception { |
||||
try { |
||||
JSONObject blockObject = client.getClient().invoke("getblockheader", new Object[]{headerHash}, JSONObject.class); |
||||
return blockObject.getInteger("height"); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getBlockHeader(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: headerHash=%s", headerHash)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用返回指定区块之后发生的与钱包相关的所有交易 |
||||
* |
||||
* @param headerHash 区块hash |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public JSONArray listSinceBlock(String headerHash) throws Exception { |
||||
try { |
||||
JSONObject blockObject = client.getClient().invoke("listsinceblock", new Object[]{headerHash}, JSONObject.class); |
||||
return blockObject.getJSONArray("transactions"); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.listSinceBlock(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: headerHash=%s", headerHash)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 用裸交易方式进行转账,可以指定找零地址,没有指定则默认输出地址 |
||||
* |
||||
* @param fromAddress 输出地址 |
||||
* @param toAddress 收入地址 |
||||
* @param amonut 数量 |
||||
* @param changeAddress 找零地址 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public String sendWithRaw(String fromAddress, String toAddress, double amonut, String changeAddress) throws Exception { |
||||
String txId = ""; |
||||
|
||||
//如果没指定找零地址则默认输出地址
|
||||
if (StringUtils.isEmpty(changeAddress)) { |
||||
changeAddress = fromAddress; |
||||
} |
||||
try { |
||||
//获取输出地址的UTXO
|
||||
JSONArray UTXOArr = listUnspent(fromAddress); |
||||
//创建裸交易
|
||||
String hexString = createRawTransaction(UTXOArr, toAddress, amonut); |
||||
//指定找零地址
|
||||
String fundHexString = fundRawTransaction(hexString, changeAddress); |
||||
//签名交易
|
||||
String signHex = signRawTransaction(fundHexString); |
||||
//广播交易
|
||||
txId = sendRawTransaction(signHex); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.sendWithRaw(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: fromAddress=%s,toAddress=%s,amonut=%s,changeAddress=%s", fromAddress, toAddress, amonut, changeAddress)); |
||||
} |
||||
return txId; |
||||
} |
||||
|
||||
/** |
||||
* 调用返回节点钱包中未确认总余额 |
||||
* |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public double getUnconfirmedBalance() throws Exception { |
||||
try { |
||||
return client.getClient().invoke("getunconfirmedbalance", null, Double.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getUnconfirmedBalance():{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用按地址分组显示余额信息 |
||||
* |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public JSONArray listAddressGroupings() throws Exception { |
||||
try { |
||||
return client.getClient().invoke("listaddressgroupings", null, JSONArray.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.listaddressgroupings():{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 通过地址获取BTC余额 |
||||
* |
||||
* @param addr 地址 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public double getBalanceByAddr(String addr) throws Exception { |
||||
try { |
||||
JSONArray jsonArray = listAddressGroupings(); |
||||
for (int i = 0; i < jsonArray.size(); i++) { |
||||
JSONArray groupArr = jsonArray.getJSONArray(i); |
||||
for (int j = 0; j < groupArr.size(); j++) { |
||||
JSONArray addrArr = groupArr.getJSONArray(j); |
||||
String addrThis = addrArr.getString(0); |
||||
if (addr.equals(addrThis)) { |
||||
return addrArr.getDoubleValue(1); |
||||
} |
||||
} |
||||
} |
||||
return 0.0; |
||||
|
||||
// double balance = 0;
|
||||
// JSONArray jsonArray = listUnspent(addr);
|
||||
// int len = jsonArray.size();
|
||||
// for (int i = 0; i < len; i++) {
|
||||
// JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||
// balance += jsonObject.getDoubleValue("amount");
|
||||
// }
|
||||
// return balance;
|
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.getBalanceByAddr(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: addr=%s", addr)); |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 调用暂时锁定/解锁指定的交易输出。一个被锁定的交易输出将不会被自动选中作为交易输入。锁定仅保持在内存中,因此节点重新启动后将自动解除锁定。 |
||||
* |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public void unLockUnspent() throws Exception { |
||||
try { |
||||
//调用返回当前暂时不可用(锁定)的UTXO清单
|
||||
JSONArray jsonArray = client.getClient().invoke("listlockunspent", null, JSONArray.class); |
||||
if (jsonArray.size() > 0) { |
||||
//解锁之前被锁定的交易输出项,设置为true解锁交易输出,否则加锁
|
||||
client.getClient().invoke("lockunspent", new Object[]{true, jsonArray}); |
||||
} |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcUtils.unLockUnspent():{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,113 +0,0 @@ |
||||
package com.blockchain.server.btc.rpc; |
||||
|
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.server.btc.common.constants.UsdtConstans; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
/** |
||||
* @author hugq |
||||
* @date 2019/2/16 15:54 |
||||
*/ |
||||
@Component |
||||
public class UsdtUtils { |
||||
|
||||
private Logger LOG = LoggerFactory.getLogger(getClass()); |
||||
|
||||
@Autowired |
||||
private BtcOmniRpcClient client; |
||||
|
||||
/** |
||||
* 创建并广播发送一个简单usdt交易 |
||||
* |
||||
* @param fromAddress 发起地址 |
||||
* @param toAddress 接受地址 |
||||
* @param amount 数量 |
||||
* @return 返回结果为16进制字符串表示的交易哈希 |
||||
* @throws Exception |
||||
*/ |
||||
public String send(String fromAddress, String toAddress, double amount) throws Exception { |
||||
String _amount = String.valueOf(amount); |
||||
try { |
||||
return client.getClient().invoke("omni_send", new Object[]{fromAddress, toAddress, UsdtConstans.USDT_PROPERTY_ID, _amount}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcOmniUtils.send(String fromAddress, String toAddress, double amount):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: fromAddress=%s,toAddress=%s,amount=%s", fromAddress, toAddress, _amount)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 创建并广播发送一个简单usdt交易,指定支付手续费地址 |
||||
* |
||||
* @param fromAddress 发起地址 |
||||
* @param toAddress 接受地址 |
||||
* @param amount 数量 |
||||
* @param feeAddress 支付手续费的地址 |
||||
* @return 返回结果为16进制字符串表示的交易哈希 |
||||
* @throws Exception |
||||
*/ |
||||
public String fundedSend(String fromAddress, String toAddress, double amount, String feeAddress) throws Exception { |
||||
String _amount = String.valueOf(amount); |
||||
try { |
||||
return client.getClient().invoke("omni_funded_send", new Object[]{fromAddress, toAddress, UsdtConstans.USDT_PROPERTY_ID, _amount, feeAddress}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcOmniUtils.fundedSend(String fromAddress, String toAddress, double amount, String feeaddress):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: fromAddress=%s,toAddress=%s,propertyId=%s,amount=%s,feeaddress=%s", fromAddress, toAddress, UsdtConstans.USDT_PROPERTY_ID, amount, feeAddress)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用创建并广播一个交易,将所有可用代币转入指定生态系统中的接收地址 |
||||
* |
||||
* @param fromAddress 发起地址 |
||||
* @param toAddress 接受地址 |
||||
* @param ecosystem 生态系统编码,数值,1 - 主生态,2 - 测试生态 |
||||
* @param feeaddress 支付手续费的地址 |
||||
* @return 返回结果为16进制字符串表示的交易哈希 |
||||
* @throws Exception |
||||
*/ |
||||
public String fundedSendAll(String fromAddress, String toAddress, int ecosystem, String feeaddress) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("omni_funded_sendall", new Object[]{fromAddress, toAddress, ecosystem, feeaddress}, String.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcOmniUtils.fundedSendAll(String fromAddress, String toAddress, int ecosystem, String feeaddress):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: fromAddress=%s,toAddress=%s,ecosystem=%s,feeaddress=%s", fromAddress, toAddress, ecosystem, feeaddress)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 调用返回指定地址和资产的代币余额 |
||||
* |
||||
* @param address 地址 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public double getBalance(String address) throws Exception { |
||||
try { |
||||
JSONObject balanceJson = client.getClient().invoke("omni_getbalance", new Object[]{address, UsdtConstans.USDT_PROPERTY_ID}, JSONObject.class); |
||||
return balanceJson.getDoubleValue("balance"); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcOmniUtils.getBalance(String address):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: address=%s", address)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取指定Omni交易的详细信息 |
||||
* |
||||
* @param txId 交易哈希 |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public JSONObject getTransaction(String txId) throws Exception { |
||||
try { |
||||
return client.getClient().invoke("omni_gettransaction", new Object[]{txId}, JSONObject.class); |
||||
} catch (Throwable e) { |
||||
LOG.info("=== BtcOmniUtils.getTransaction(String):{} ===", e.getMessage(), e); |
||||
throw new Exception(e.getMessage() + String.format("[params]: txId=%s", txId)); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,280 +0,0 @@ |
||||
package com.blockchain.server.btc.scheduleTask; |
||||
|
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.server.btc.common.constants.BtcBlockNumberConstans; |
||||
import com.blockchain.server.btc.common.constants.BtcConstans; |
||||
import com.blockchain.server.btc.common.constants.BtcTransferConstans; |
||||
import com.blockchain.server.btc.common.constants.UsdtConstans; |
||||
import com.blockchain.server.btc.common.util.BtcAddressSetRedisUtils; |
||||
import com.blockchain.server.btc.common.util.BtcBlockRedisUtils; |
||||
import com.blockchain.server.btc.dto.BtcBlockNumberDTO; |
||||
import com.blockchain.server.btc.entity.BtcWalletTransfer; |
||||
import com.blockchain.server.btc.rpc.BtcUtils; |
||||
import com.blockchain.server.btc.rpc.UsdtUtils; |
||||
import com.blockchain.server.btc.service.BtcBlockNumberService; |
||||
import com.blockchain.server.btc.service.BtcWalletTransferService; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.Date; |
||||
import java.util.List; |
||||
import java.util.UUID; |
||||
|
||||
//@Component
|
||||
public class RechargeBtcAndUsdtBlockTimer { |
||||
|
||||
@Autowired |
||||
private BtcAddressSetRedisUtils btcAddressSetRedisUtils; |
||||
|
||||
@Autowired |
||||
private BtcBlockRedisUtils btcBlockRedisUtils; |
||||
|
||||
@Autowired |
||||
private BtcUtils btcUtils; |
||||
|
||||
@Autowired |
||||
private UsdtUtils usdtUtils; |
||||
|
||||
@Autowired |
||||
private BtcBlockNumberService btcBlockNumberService; |
||||
|
||||
@Autowired |
||||
private BtcWalletTransferService btcWalletTransferService; |
||||
|
||||
/** |
||||
* 查询区块链充值信息 |
||||
*/ |
||||
@Scheduled(cron = "0/30 * * * * ?") //30秒每次
|
||||
public void rechargeBtcAndUsdtBlock() { |
||||
//获取已经同步的最大区块号
|
||||
int bigestBlock = btcBlockNumberService.selectBigest(); |
||||
//区块链最新区块高度
|
||||
int blockHeightRpc = 0; |
||||
//获取该区块hash
|
||||
String blockHash = ""; |
||||
//交易信息
|
||||
JSONArray transferArr = null; |
||||
try { |
||||
blockHeightRpc = btcUtils.getBlockCount(); |
||||
//是否已经同步到最新区块
|
||||
if (bigestBlock >= blockHeightRpc) { |
||||
return; |
||||
} |
||||
// //判断是否有未确认交易
|
||||
// if (btcUtils.getUnconfirmedBalance() != 0) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
blockHash = btcUtils.getBlockHash(bigestBlock); |
||||
transferArr = btcUtils.listSinceBlock(blockHash); |
||||
} catch (Exception e) { |
||||
return; |
||||
} |
||||
|
||||
//区块号自增
|
||||
bigestBlock++; |
||||
//保存已经同步区块高度的到数据库
|
||||
btcBlockNumberService.insertBlockNumber(bigestBlock, BtcBlockNumberConstans.STATUS_W); |
||||
|
||||
int transferArrLen = transferArr.size(); |
||||
for (int i = 0; i < transferArrLen; i++) { |
||||
JSONObject transferObj = transferArr.getJSONObject(i); |
||||
if ("receive".equals(transferObj.getString("category"))) { |
||||
String toAddr = transferObj.getString("address"); |
||||
if (btcAddressSetRedisUtils.isExistsAddr(toAddr)) { |
||||
String txId = transferObj.getString("txid"); |
||||
|
||||
try { |
||||
//先用usdt解析,如果不能解析,则用btc解析
|
||||
JSONObject transferUsdt = usdtUtils.getTransaction(txId); |
||||
try { |
||||
//修改该usdt钱包余额,并插入一条充值记录
|
||||
parseUsdtTransfer(transferUsdt, txId); |
||||
} catch (Exception e) { |
||||
return; |
||||
} |
||||
} catch (Exception e) { |
||||
//btc解析
|
||||
try { |
||||
JSONObject transferBtc = btcUtils.getTransaction(txId); |
||||
parseBtcTransfer(transferBtc, txId); |
||||
} catch (Exception e1) { |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
//修改区块同步状态
|
||||
btcBlockNumberService.updateStatus(bigestBlock, BtcBlockNumberConstans.STATUS_Y); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 查询区块链遗漏的充值信息 |
||||
*/ |
||||
@Scheduled(cron = "0 0/2 * * * ?") //2分钟每次
|
||||
public void crawlOmitTxIn() { |
||||
long nowTime = System.currentTimeMillis(); |
||||
//获取等待区块列表
|
||||
List<BtcBlockNumberDTO> blockNumberDTOList = btcBlockNumberService.listByStatus(BtcBlockNumberConstans.STATUS_W); |
||||
for (BtcBlockNumberDTO blockNumberDTO : blockNumberDTOList) { |
||||
//判断时间差
|
||||
if (nowTime - blockNumberDTO.getCreateTime().getTime() < BtcBlockNumberConstans.TIME_DIFFERENCE) { |
||||
continue; |
||||
} |
||||
//获取爬取次数
|
||||
int count = btcBlockRedisUtils.getBlockOptMapCount(blockNumberDTO.getBlockNumber()); |
||||
if (count >= 5) { // 爬取次数超过五次跳出
|
||||
//删除缓存
|
||||
btcBlockRedisUtils.delBlockOptMapCount(blockNumberDTO.getBlockNumber()); |
||||
//记录改为失败
|
||||
btcBlockNumberService.updateStatus(blockNumberDTO.getBlockNumber(), BtcBlockNumberConstans.STATUS_N); |
||||
continue; |
||||
} |
||||
|
||||
try { |
||||
//本区块hash
|
||||
String thisBlockHash = btcUtils.getBlockHash(blockNumberDTO.getBlockNumber()); |
||||
//上一个区块hash
|
||||
String previousBlockHash = btcUtils.getBlockHash(blockNumberDTO.getBlockNumber() - 1); |
||||
//交易数组
|
||||
JSONArray transferArr = btcUtils.listSinceBlock(previousBlockHash); |
||||
|
||||
int transferArrLen = transferArr.size(); |
||||
for (int i = 0; i < transferArrLen; i++) { |
||||
JSONObject transferObj = transferArr.getJSONObject(i); |
||||
//不为本次区块交易,不需要解析
|
||||
if (!thisBlockHash.equals(transferObj.getString("blockhash"))) { |
||||
continue; |
||||
} |
||||
if ("receive".equals(transferObj.getString("category"))) { |
||||
String toAddr = transferObj.getString("address"); |
||||
if (btcAddressSetRedisUtils.isExistsAddr(toAddr)) { |
||||
String txId = transferObj.getString("txid"); |
||||
|
||||
try { |
||||
//先用usdt解析,如果不能解析,则用btc解析
|
||||
JSONObject transferUsdt = usdtUtils.getTransaction(txId); |
||||
try { |
||||
//修改该usdt钱包余额,并插入一条充值记录
|
||||
parseUsdtTransfer(transferUsdt, txId); |
||||
} catch (Exception e) { |
||||
continue; |
||||
} |
||||
} catch (Exception e) { |
||||
//btc解析
|
||||
try { |
||||
JSONObject transferBtc = btcUtils.getTransaction(txId); |
||||
parseBtcTransfer(transferBtc, txId); |
||||
} catch (Exception e1) { |
||||
continue; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} catch (Exception e) { |
||||
continue; |
||||
} |
||||
|
||||
//爬取次数加1
|
||||
btcBlockRedisUtils.incrementBlockOptMapCount(blockNumberDTO.getBlockNumber()); |
||||
try { |
||||
//修改区块同步状态
|
||||
btcBlockNumberService.updateStatus(blockNumberDTO.getBlockNumber(), BtcBlockNumberConstans.STATUS_Y); |
||||
//删除缓存
|
||||
btcBlockRedisUtils.delBlockOptMapCount(blockNumberDTO.getBlockNumber()); |
||||
} catch (Exception e) { |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 解析btc交易 |
||||
* |
||||
* @param transferBtc btc交易 |
||||
* @param txId 交易id |
||||
*/ |
||||
private void parseBtcTransfer(JSONObject transferBtc, String txId) { |
||||
//交易金额,正数表示该交易增加钱包余额,负数表示该交易减少钱包余额
|
||||
if (transferBtc.getDouble("amount") <= 0) { |
||||
return; |
||||
} |
||||
|
||||
JSONArray details = transferBtc.getJSONArray("details"); |
||||
String toAddr = null; |
||||
String fromAddr = null; |
||||
double amount = 0; |
||||
double fee = 0; |
||||
|
||||
for (int i = 0; i < details.size(); i++) { |
||||
JSONObject jsonObject = details.getJSONObject(i); // 遍历 jsonarray 数组,把每一个对象转成 json 对象
|
||||
String category = jsonObject.getString("category"); |
||||
if ("receive".equals(category)) { |
||||
toAddr = jsonObject.getString("address"); |
||||
amount = jsonObject.getDouble("amount"); |
||||
} else if ("send".equals(category)) { |
||||
fromAddr = jsonObject.getString("address"); |
||||
fee = Math.abs(jsonObject.getDouble("fee")); |
||||
} |
||||
} |
||||
|
||||
if (btcAddressSetRedisUtils.isExistsAddr(toAddr)) { |
||||
//修改该钱包余额,并插入一条充值记录
|
||||
BtcWalletTransfer btcWalletTransfer = new BtcWalletTransfer(); |
||||
btcWalletTransfer.setId(UUID.randomUUID().toString()); |
||||
btcWalletTransfer.setHash(txId); |
||||
btcWalletTransfer.setFromAddr(fromAddr); |
||||
btcWalletTransfer.setToAddr(toAddr); |
||||
btcWalletTransfer.setAmount(Math.abs(amount)); |
||||
btcWalletTransfer.setGasPrice(fee); |
||||
btcWalletTransfer.setTokenId(BtcConstans.BTC_PROPERTY_ID); |
||||
btcWalletTransfer.setTokenSymbol(BtcConstans.BTC_SYMBOL); |
||||
btcWalletTransfer.setTransferType(BtcTransferConstans.TYPE_IN); |
||||
btcWalletTransfer.setStatus(BtcTransferConstans.STATUS_SUCCESS); |
||||
btcWalletTransfer.setCreateTime(new Date()); |
||||
btcWalletTransfer.setUpdateTime(btcWalletTransfer.getCreateTime()); |
||||
btcWalletTransferService.handleBlockRecharge(btcWalletTransfer); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 解析usdt交易 |
||||
* |
||||
* @param transferUsdt usdt交易 |
||||
* @param txId 交易id |
||||
*/ |
||||
private void parseUsdtTransfer(JSONObject transferUsdt, String txId) { |
||||
//交易是否有效交易
|
||||
if (transferUsdt.getBoolean("valid")) { |
||||
//是否USDT交易
|
||||
if (transferUsdt.getInteger("propertyid") == UsdtConstans.USDT_PROPERTY_ID) { |
||||
//是否充值到本地钱包地址
|
||||
String toAddr = transferUsdt.getString("referenceaddress"); |
||||
if (btcAddressSetRedisUtils.isExistsAddr(toAddr)) { |
||||
//修改该钱包余额,并插入一条充值记录
|
||||
BtcWalletTransfer btcWalletTransfer = new BtcWalletTransfer(); |
||||
btcWalletTransfer.setId(UUID.randomUUID().toString()); |
||||
btcWalletTransfer.setHash(txId); |
||||
btcWalletTransfer.setFromAddr(transferUsdt.getString("sendingaddress")); |
||||
btcWalletTransfer.setToAddr(toAddr); |
||||
btcWalletTransfer.setAmount(Math.abs(transferUsdt.getDouble("amount"))); |
||||
btcWalletTransfer.setGasPrice(transferUsdt.getDouble("fee")); |
||||
btcWalletTransfer.setTokenId(UsdtConstans.USDT_PROPERTY_ID); |
||||
btcWalletTransfer.setTokenSymbol(UsdtConstans.USDT_SYMBOL); |
||||
btcWalletTransfer.setTransferType(BtcTransferConstans.TYPE_IN); |
||||
btcWalletTransfer.setStatus(BtcTransferConstans.STATUS_SUCCESS); |
||||
btcWalletTransfer.setCreateTime(new Date()); |
||||
btcWalletTransfer.setUpdateTime(btcWalletTransfer.getCreateTime()); |
||||
btcWalletTransferService.handleBlockRecharge(btcWalletTransfer); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,86 +0,0 @@ |
||||
package com.blockchain.server.btc.scheduleTask; |
||||
|
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.server.btc.common.constants.BtcConstans; |
||||
import com.blockchain.server.btc.common.constants.BtcTransferConstans; |
||||
import com.blockchain.server.btc.entity.BtcWalletTransfer; |
||||
import com.blockchain.server.btc.rpc.BtcUtils; |
||||
import com.blockchain.server.btc.rpc.UsdtUtils; |
||||
import com.blockchain.server.btc.service.BtcWalletService; |
||||
import com.blockchain.server.btc.service.BtcWalletTransferService; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.List; |
||||
import java.util.concurrent.ExecutorService; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
/** |
||||
* 钱包业务处理的定时器 |
||||
*/ |
||||
//@Component
|
||||
public class WallertTimerTask { |
||||
|
||||
|
||||
// 钱包提现处理
|
||||
private static ExecutorService txOutExecutorService = Executors.newSingleThreadExecutor(); |
||||
// 日志
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WallertTimerTask.class); |
||||
|
||||
@Autowired |
||||
BtcWalletTransferService walletTransferService; |
||||
@Autowired |
||||
BtcWalletService btcWalletService; |
||||
@Autowired |
||||
BtcUtils btcUtils; |
||||
@Autowired |
||||
UsdtUtils usdtUtils; |
||||
|
||||
@Scheduled(cron = "0/30 * * * * ?") |
||||
public void crawlTxOut() { |
||||
txOutExecutorService.execute(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
crawlTxOutDispose(); // 爬取提现记录
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
void crawlTxOutDispose() { |
||||
// 查询提现的未处理的数据
|
||||
List<BtcWalletTransfer> txs = walletTransferService.selectByTxTypeAndStatus(BtcTransferConstans.TYPE_OUT, |
||||
BtcTransferConstans.STATUS_ALREADY_OUT, 0, 99); |
||||
for (BtcWalletTransfer tx : txs) { |
||||
try { |
||||
if (BtcConstans.BTC_SYMBOL.equalsIgnoreCase(tx.getTokenSymbol())) { |
||||
JSONObject obj = btcUtils.getTransaction(tx.getHash()); |
||||
int confirmations = new Integer(obj.get("confirmations").toString()).intValue(); |
||||
if (confirmations >= 1) { |
||||
btcWalletService.updateTxOutSuccess(tx); |
||||
}else{ |
||||
btcWalletService.updateTxOutError(tx); |
||||
|
||||
} |
||||
} else { |
||||
JSONObject obj = usdtUtils.getTransaction(tx.getHash()); |
||||
int confirmations = new Integer(obj.get("confirmations").toString()).intValue(); |
||||
boolean valid = Boolean.getBoolean(obj.get("valid").toString()); |
||||
if (!valid) { |
||||
btcWalletService.updateTxOutError(tx); |
||||
} else if (valid && confirmations >= 1) { |
||||
btcWalletService.updateTxOutSuccess(tx); |
||||
} |
||||
} |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
continue; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
} |
@ -1,40 +0,0 @@ |
||||
package com.blockchain.server.btc.service; |
||||
|
||||
import com.blockchain.server.btc.dto.BtcBlockNumberDTO; |
||||
|
||||
import java.util.List; |
||||
|
||||
public interface BtcBlockNumberService { |
||||
|
||||
/** |
||||
* 新增已经同步的区块,不成功报异常 |
||||
* |
||||
* @param blockNumber 区块高度 |
||||
* @param status 状态 |
||||
* @return |
||||
*/ |
||||
void insertBlockNumber(int blockNumber, String status); |
||||
|
||||
/** |
||||
* 修改区块装填,不成功报异常 |
||||
* |
||||
* @param blockNumber 区块高度 |
||||
* @return |
||||
*/ |
||||
void updateStatus(int blockNumber, String status); |
||||
|
||||
/** |
||||
* 根据状态获取同步区块 |
||||
* |
||||
* @param status 状态 |
||||
* @return |
||||
*/ |
||||
List<BtcBlockNumberDTO> listByStatus(String status); |
||||
|
||||
/** |
||||
* 查询已经同步的最大区块号 |
||||
* |
||||
* @return |
||||
*/ |
||||
int selectBigest(); |
||||
} |
@ -1,7 +0,0 @@ |
||||
package com.blockchain.server.btc.service; |
||||
|
||||
public interface BtcWalletKeyService { |
||||
|
||||
void insertWalletKey(String address, String privateKey); |
||||
|
||||
} |
@ -1,68 +0,0 @@ |
||||
package com.blockchain.server.btc.service.impl; |
||||
|
||||
import com.blockchain.server.btc.common.constants.BtcBlockNumberConstans; |
||||
import com.blockchain.server.btc.common.enums.BtcEnums; |
||||
import com.blockchain.server.btc.common.exception.BtcException; |
||||
import com.blockchain.server.btc.dto.BtcBlockNumberDTO; |
||||
import com.blockchain.server.btc.entity.BtcBlockNumber; |
||||
import com.blockchain.server.btc.mapper.BtcBlockNumberMapper; |
||||
import com.blockchain.server.btc.rpc.BtcUtils; |
||||
import com.blockchain.server.btc.service.BtcBlockNumberService; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.util.Date; |
||||
import java.util.List; |
||||
|
||||
@Service |
||||
public class BtcBlockNumberServiceImpl implements BtcBlockNumberService { |
||||
@Autowired |
||||
private BtcBlockNumberMapper btcBlockNumberMapper; |
||||
@Autowired |
||||
private BtcUtils btcUtils; |
||||
|
||||
@Override |
||||
public void insertBlockNumber(int blockNumber, String status) { |
||||
BtcBlockNumber btcBlockNumber = new BtcBlockNumber(); |
||||
btcBlockNumber.setBlockNumber(blockNumber); |
||||
btcBlockNumber.setStatus(status); |
||||
btcBlockNumber.setCreateTime(new Date()); |
||||
btcBlockNumber.setUpdateTime(btcBlockNumber.getCreateTime()); |
||||
int count = btcBlockNumberMapper.insertSelective(btcBlockNumber); |
||||
if (count != 1) { |
||||
throw new BtcException(BtcEnums.INSERT_BLOCKNUMBER_ERROR); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void updateStatus(int blockNumber, String status) { |
||||
BtcBlockNumber btcBlockNumber = new BtcBlockNumber(); |
||||
btcBlockNumber.setBlockNumber(blockNumber); |
||||
btcBlockNumber.setStatus(status); |
||||
btcBlockNumber.setUpdateTime(new Date()); |
||||
int countRow = btcBlockNumberMapper.updateByPrimaryKeySelective(btcBlockNumber); |
||||
if (countRow != 1) { |
||||
throw new BtcException(BtcEnums.UPDATE_BLOCK_NUMBER_STATUS_ERROR); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public List<BtcBlockNumberDTO> listByStatus(String status) { |
||||
return btcBlockNumberMapper.listByStatus(status); |
||||
} |
||||
|
||||
@Override |
||||
public int selectBigest() { |
||||
Integer blockNumber = btcBlockNumberMapper.selectBigest(); |
||||
if (blockNumber == null) { |
||||
try { |
||||
blockNumber = btcUtils.getBlockCount(); |
||||
this.insertBlockNumber(blockNumber, BtcBlockNumberConstans.STATUS_Y); |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
return blockNumber; |
||||
} |
||||
|
||||
} |
@ -1,33 +0,0 @@ |
||||
package com.blockchain.server.btc.service.impl; |
||||
|
||||
import com.blockchain.server.btc.common.enums.BtcEnums; |
||||
import com.blockchain.server.btc.common.exception.BtcException; |
||||
import com.blockchain.server.btc.common.util.BtcRASUtils; |
||||
import com.blockchain.server.btc.entity.BtcWalletKey; |
||||
import com.blockchain.server.btc.mapper.BtcWalletKeyMapper; |
||||
import com.blockchain.server.btc.service.BtcWalletKeyService; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
@Service |
||||
public class BtcWalletKeyServiceImpl implements BtcWalletKeyService { |
||||
@Autowired |
||||
private BtcWalletKeyMapper btcWalletKeyMapper; |
||||
|
||||
@Override |
||||
public void insertWalletKey(String address, String privateKey) { |
||||
BtcWalletKey btcWalletKey = new BtcWalletKey(); |
||||
btcWalletKey.setAddr(address); |
||||
try { |
||||
privateKey = BtcRASUtils.encrypt(privateKey); |
||||
} catch (Exception e) { |
||||
throw new BtcException(BtcEnums.CREATE_WALLET_ERROR); |
||||
} |
||||
btcWalletKey.setPrivateKey(privateKey); |
||||
int countIw = btcWalletKeyMapper.insertSelective(btcWalletKey); |
||||
if (countIw != 1) { |
||||
throw new BtcException(BtcEnums.CREATE_WALLET_ERROR); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,27 +0,0 @@ |
||||
<?xml version="1.0" encoding="UTF-8" ?> |
||||
<!DOCTYPE mapper |
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="com.blockchain.server.btc.mapper.BtcBlockNumberMapper"> |
||||
<resultMap id="BtcBlockNumberMap" type="com.blockchain.server.btc.dto.BtcBlockNumberDTO"> |
||||
<result property="createTime" column="create_time"/> |
||||
<result property="blockNumber" column="block_number"/> |
||||
<result property="updateTime" column="update_time"/> |
||||
<result property="status" column="status"/> |
||||
</resultMap> |
||||
|
||||
<sql id="tableName">dapp_btc_block_number</sql> |
||||
|
||||
<select id="selectBigest" resultType="java.lang.Integer"> |
||||
SELECT MAX(block_number) |
||||
FROM <include refid="tableName"/> |
||||
</select> |
||||
|
||||
<select id="listByStatus" resultMap="BtcBlockNumberMap"> |
||||
SELECT block_number, create_time |
||||
FROM <include refid="tableName"/> |
||||
WHERE status = #{status} |
||||
order by block_number asc |
||||
</select> |
||||
|
||||
</mapper> |
@ -1,11 +0,0 @@ |
||||
<?xml version="1.0" encoding="UTF-8" ?> |
||||
<!DOCTYPE mapper |
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="com.blockchain.server.btc.mapper.BtcWalletKeyMapper"> |
||||
<resultMap id="BtcWalletKeyMap" type="com.blockchain.server.btc.dto.BtcWalletKeyDTO"> |
||||
<result property="privateKey" column="private_key"/> |
||||
<result property="addr" column="addr"/> |
||||
</resultMap> |
||||
|
||||
</mapper> |
@ -1,178 +0,0 @@ |
||||
package com.blockchain.server.eos.common.util; |
||||
|
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.server.eos.common.constant.EosConstant; |
||||
import com.blockchain.server.eos.common.enums.EosWalletEnums; |
||||
import com.blockchain.server.eos.common.exception.EosWalletException; |
||||
import com.blockchain.server.eos.dto.WalletInDTO; |
||||
import com.blockchain.server.eos.entity.Wallet; |
||||
import com.blockchain.server.eos.entity.WalletTransfer; |
||||
import com.blockchain.server.eos.service.EosWalletService; |
||||
import com.blockchain.server.eos.service.EosWalletTransferService; |
||||
import com.blockchain.server.eos.service.EosWalletUtilService; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.math.BigInteger; |
||||
|
||||
/** |
||||
* @author Harvey Luo |
||||
* @date 2019/5/27 10:54 |
||||
*/ |
||||
@Component |
||||
public class EosUtil { |
||||
|
||||
@Autowired |
||||
private EosWalletService eosWalletService; |
||||
@Autowired |
||||
private EosWalletTransferService eosWalletTransferService; |
||||
@Autowired |
||||
private EosWalletUtilService eosWalletUtilService; |
||||
|
||||
// 日志
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EosUtil.class); |
||||
|
||||
/** |
||||
* 递归方式判断查询返回账户历史数据 |
||||
* |
||||
* @param walletInDTO |
||||
* @param offset |
||||
* @return |
||||
*/ |
||||
public JSONArray recursion(WalletInDTO walletInDTO, Integer offset) { |
||||
JSONObject bodyBody = eosWalletUtilService.getActions(walletInDTO.getAccountName(), offset.toString()); |
||||
if (bodyBody == null) return null; |
||||
JSONArray actions = bodyBody.getJSONArray("actions"); |
||||
BigInteger blockNumber = walletInDTO.getBlockNumber(); |
||||
if (actions.size() > 0) { |
||||
JSONObject job = actions.getJSONObject(0); |
||||
JSONObject action_trace = job.getJSONObject("action_trace"); |
||||
BigInteger blockNum = action_trace.getBigInteger("block_num"); |
||||
if (blockNum.compareTo(blockNumber) <= 0 || offset.equals(EosConstant.RPCConstant.OFFSET_MAX)) |
||||
return actions; |
||||
else { |
||||
return recursion(walletInDTO, offset + EosConstant.RPCConstant.ADD_OFFSET); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* 分析账户历史记录 |
||||
* |
||||
* @param actions |
||||
* @param tokenName |
||||
* @param walletInDTO |
||||
*/ |
||||
public void handTransaction(JSONArray actions, String tokenName, WalletInDTO walletInDTO) { |
||||
if (actions.size() > 0) { |
||||
for (int i = 0; i < actions.size(); i++) { |
||||
JSONObject job = actions.getJSONObject(i); |
||||
JSONObject action_trace = job.getJSONObject("action_trace"); |
||||
JSONObject act = action_trace.getJSONObject("act"); |
||||
String token = act.getObject("account", String.class); |
||||
// 判断币种地址
|
||||
if (!token.equalsIgnoreCase(tokenName)) continue; |
||||
JSONObject data = act.getJSONObject("data"); |
||||
String to = data.getObject("to", String.class); |
||||
// 判断资金账户
|
||||
if (to == null || !walletInDTO.getAccountName().equalsIgnoreCase(to)) continue; |
||||
// 判断是否为本地钱包
|
||||
String memo = data.getObject("memo", String.class); |
||||
Integer walletId = null; |
||||
try { |
||||
walletId = Integer.parseInt(memo.trim()); |
||||
} catch (Exception e) { |
||||
continue; |
||||
} |
||||
Wallet wallet = eosWalletService.selectWalletById(walletId.toString()); |
||||
if (wallet == null) continue; |
||||
// 获取充值金额和币种符号
|
||||
String quantity = data.getObject("quantity", String.class); |
||||
String[] quantitys = quantity.split(" "); |
||||
String amount = quantitys[0]; |
||||
String symbol = quantitys[1]; |
||||
if (!wallet.getTokenSymbol().equalsIgnoreCase(symbol)) continue; |
||||
|
||||
String hash = action_trace.getString("trx_id"); |
||||
// 判断数据库是否存在该记录
|
||||
Integer row = eosWalletTransferService.countByHashAndTransferType(hash, EosConstant.TransferType.TRANSFER_IN); |
||||
if (row != 0) continue; |
||||
String from = data.getObject("from", String.class); |
||||
BigInteger blockNum = action_trace.getBigInteger("block_num"); |
||||
// 修改钱包插入充值记录
|
||||
eosWalletTransferService.handleWalletAndWalletTransfer(hash, |
||||
wallet.getId(), |
||||
null, |
||||
EosConstant.TransferType.TRANSFER_IN, |
||||
new BigDecimal(quantitys[0]), |
||||
tokenName, |
||||
quantitys[1], |
||||
memo, |
||||
blockNum); |
||||
LOG.info("************************充值成功: hash值 ==》 " + hash + " + 币种名称 ==》 " + tokenName + " + 钱包地址 ==》 " + wallet.getId() + " + 金额 ==》" + quantity + "************************"); |
||||
|
||||
// System.out.println("hashId ===>" + hash);
|
||||
// System.out.println("blockNum ===>" + blockNum);
|
||||
// System.out.println("amount ==> " + amount);
|
||||
// System.out.println("symbol ==> " + symbol);
|
||||
// System.out.println("from ==> " + from);
|
||||
// System.out.println("to ==> " + to);
|
||||
// System.out.println("quantity ==> " + quantity);
|
||||
// System.out.println("memo ==> " + memo);
|
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 解析eos查询交易信息状态 |
||||
* |
||||
* @param blockNum |
||||
* @param hashId |
||||
* @param walletTransfer |
||||
* @return |
||||
*/ |
||||
public Boolean getTransactions(String blockNum, String hashId, WalletTransfer walletTransfer) { |
||||
JSONObject body = eosWalletUtilService.getTransaction(blockNum, hashId); |
||||
if (body == null || "".equalsIgnoreCase(body.toString())) { |
||||
LOG.info("************************出币失败:区块高度" + blockNum + "交易hash:" + hashId + "************************"); |
||||
return false; |
||||
} |
||||
JSONObject action = null; |
||||
JSONObject data = null; |
||||
try { |
||||
// 返回结果分析出币信息
|
||||
JSONObject trx = body.getJSONObject("trx"); |
||||
JSONObject trx1 = trx.getJSONObject("trx"); |
||||
JSONArray actions = trx1.getJSONArray("actions"); |
||||
action = actions.getJSONObject(0); |
||||
data = action.getJSONObject("data"); |
||||
if (null == data || "".equalsIgnoreCase(data.toString())) { |
||||
LOG.info("************************出币失败:区块高度" + blockNum + "交易hash:" + hashId + "************************"); |
||||
return false; |
||||
} else { |
||||
// 支付人地址:
|
||||
// String from = data.getObject("from", String.class);
|
||||
// 收款人地址:
|
||||
String to = data.getObject("to", String.class); |
||||
// 提现由系统账户转账到用户提供账户
|
||||
// 判断to是否为用户提供的地址名称
|
||||
if (to.equalsIgnoreCase(walletTransfer.getAccountName())) return true; |
||||
else { |
||||
LOG.info("************************出币失败:区块高度" + blockNum + "交易hash:" + hashId + "************************"); |
||||
throw new EosWalletException(EosWalletEnums.CURRENCY_FAILURE_ERROR); |
||||
} |
||||
} |
||||
} catch (Exception e) { |
||||
LOG.info("************************出币失败:区块高度" + blockNum + "交易hash:" + hashId + "************************"); |
||||
// TODO 出币失败是否抛出错误
|
||||
// throw new EosWalletException(EosWalletEnums.CURRENCY_FAILURE_ERROR);
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,66 +0,0 @@ |
||||
package com.blockchain.server.eos.mapper; |
||||
|
||||
import com.blockchain.server.eos.dto.BlockNumberDTO; |
||||
import com.blockchain.server.eos.entity.BlockNumber; |
||||
import org.apache.ibatis.annotations.Param; |
||||
import org.springframework.stereotype.Repository; |
||||
import tk.mybatis.mapper.common.Mapper; |
||||
|
||||
import java.math.BigInteger; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* BlockNumberMapper 数据访问类 |
||||
* @date 2018-11-05 15:10:47 |
||||
* @version 1.0 |
||||
*/ |
||||
@Repository |
||||
public interface BlockNumberMapper extends Mapper<BlockNumber> { |
||||
|
||||
/** |
||||
* 查询最旧区块 |
||||
* @return |
||||
*/ |
||||
BigInteger selectMinBlockNum(); |
||||
/** |
||||
* 查询最大的区块号 |
||||
* @return |
||||
*/ |
||||
BigInteger selectMaxBlockNum(); |
||||
|
||||
/** |
||||
* 查询是否存在区块 |
||||
* @param blockNum |
||||
* @return |
||||
*/ |
||||
BlockNumberDTO selectBlockNumIsExist(@Param("blockNum") BigInteger blockNum); |
||||
|
||||
/** |
||||
* 查找当前区块状态 |
||||
* @param blockNum |
||||
* @return |
||||
*/ |
||||
Character selectBlockNumStatusByBlockNum(@Param("blockNum") BigInteger blockNum); |
||||
|
||||
/** |
||||
* 根据区块状态获取区块 |
||||
* @param status |
||||
* @return |
||||
*/ |
||||
List<BigInteger> listBlockNumberByStatus(@Param("status") Character status); |
||||
|
||||
/** |
||||
* 更新处理区块状态 |
||||
* @param blockNum |
||||
* @param status |
||||
* @return |
||||
*/ |
||||
int updateBlockNumberByStatus(@Param("blockNum") BigInteger blockNum, @Param("status") Character status); |
||||
|
||||
/** |
||||
* |
||||
* @param blockNumber |
||||
* @return |
||||
*/ |
||||
int insertSelectiveIgnore(BlockNumber blockNumber); |
||||
} |
@ -1,54 +0,0 @@ |
||||
package com.blockchain.server.eos.scheduleTask; |
||||
|
||||
import com.blockchain.server.eos.service.EosTokenService; |
||||
import com.blockchain.server.eos.service.EosWalletInService; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.HashSet; |
||||
import java.util.concurrent.ExecutorService; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
//@Component
|
||||
public class TransferDisposeTimerTask { |
||||
@Autowired |
||||
private EosTokenService tokenService; |
||||
@Autowired |
||||
private EosWalletInService eosWalletInService; |
||||
|
||||
private static ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); |
||||
|
||||
// 日志
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TransferDisposeTimerTask.class); |
||||
|
||||
/** |
||||
* 查eos区块信息 |
||||
*/ |
||||
@Scheduled(cron = "0/10 * * * * ?") //5秒钟执行一次 */5 * * * * ? 5分钟执行一次 0 */5 * * * ?
|
||||
public void selectTxStatus() { |
||||
LOG.info("************************eos交易记录爬取的定时任务************************"); |
||||
singleThreadExecutor.execute(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
dispose(); //查询eos账户历史充值信息
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
public void dispose() { |
||||
// Token查询所有代币
|
||||
HashSet<String> listTokenName = tokenService.listTokenNameAll(); |
||||
try { |
||||
// 充值处理方法
|
||||
eosWalletInService.handleAccountBlockTransactions(listTokenName); |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
LOG.error("************************ 充值处理异常 ************************"); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,64 +0,0 @@ |
||||
package com.blockchain.server.eos.scheduleTask; |
||||
|
||||
import com.blockchain.server.eos.common.constant.EosConstant; |
||||
import com.blockchain.server.eos.entity.WalletTransfer; |
||||
import com.blockchain.server.eos.eos4j.Rpc; |
||||
import com.blockchain.server.eos.service.EosWalletService; |
||||
import com.blockchain.server.eos.service.EosWalletTransferService; |
||||
import com.blockchain.server.eos.service.EosWalletUtilService; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.List; |
||||
import java.util.concurrent.ExecutorService; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
/** |
||||
* 钱包业务处理的定时器 |
||||
*/ |
||||
//@Component
|
||||
public class WallertTimerTask { |
||||
|
||||
// 钱包提现处理
|
||||
private static ExecutorService txOutExecutorService = Executors.newSingleThreadExecutor(); |
||||
// 日志
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WallertTimerTask.class); |
||||
|
||||
@Autowired |
||||
EosWalletTransferService walletTransferService; |
||||
@Autowired |
||||
EosWalletService walletService; |
||||
|
||||
@Scheduled(cron = "0/5 * * * * ?") |
||||
public void crawlTxOut() { |
||||
txOutExecutorService.execute(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
crawlTxOutDispose(); // 爬取提现记录
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
void crawlTxOutDispose() { |
||||
// 查询提现的未处理的数据
|
||||
List<WalletTransfer> txs = walletTransferService.selectByTxTypeAndStatus(EosConstant.TransferType.TRANSFER_OUT, EosConstant.TransferStatus.YI_CHU_BI, 0, 99); |
||||
for (WalletTransfer tx : txs) { |
||||
try { |
||||
Boolean status = walletService.getTransaction(tx.getBlockNumber(), tx.getHash()); |
||||
// 修改失败状态
|
||||
if (!status) walletService.updateTxOutError(tx); |
||||
// 修改余额改变成功状态
|
||||
else walletService.updateTxOutSuccess(tx); |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
continue; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
} |
@ -1,59 +0,0 @@ |
||||
package com.blockchain.server.eos.service; |
||||
|
||||
import com.blockchain.server.eos.dto.BlockNumberDTO; |
||||
import com.blockchain.server.eos.entity.BlockNumber; |
||||
|
||||
import java.math.BigInteger; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author Harvey |
||||
* @date 2019/2/16 17:45 |
||||
* @user WIN10 |
||||
*/ |
||||
public interface EosBlockNumberService { |
||||
|
||||
/** |
||||
* 查询最大区块 |
||||
* @return |
||||
*/ |
||||
BigInteger selectCurrentBlockNum(); |
||||
|
||||
/** |
||||
* 添加区块交易信息 |
||||
* @param nowBlock |
||||
* @param listTokenAddr |
||||
*/ |
||||
void handleBlockTransactions(BigInteger nowBlock, HashSet<String> listTokenAddr); |
||||
|
||||
/** |
||||
* 添加 |
||||
* @param blockNumber |
||||
* @return |
||||
*/ |
||||
int insertBlockNumber(BlockNumber blockNumber); |
||||
|
||||
/** |
||||
* 插入区块处理状态 |
||||
* @param nowBlock |
||||
* @param status |
||||
* @return |
||||
*/ |
||||
int insertBlockNumberByStatus(BigInteger nowBlock, Character status); |
||||
|
||||
/** |
||||
* 根据区块状态获取区块 |
||||
* @param eosBlockNumberWait |
||||
* @return |
||||
*/ |
||||
List<BigInteger> listBlockNumberByStatus(Character eosBlockNumberWait); |
||||
|
||||
/** |
||||
* 更新处理区块状态 |
||||
* @param blockNum |
||||
* @param status |
||||
* @return |
||||
*/ |
||||
int updateBlockNumberByStatus(BigInteger blockNum, Character status); |
||||
} |
@ -1,48 +0,0 @@ |
||||
package com.blockchain.server.eos.service; |
||||
|
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.server.eos.dto.BlockNumberDTO; |
||||
|
||||
import java.math.BigInteger; |
||||
|
||||
/** |
||||
* @author Harvey |
||||
* @date 2019/2/18 13:57 |
||||
* @user WIN10 |
||||
*/ |
||||
public interface EosWalletUtilService { |
||||
|
||||
/** |
||||
* 获取最新区块 |
||||
* @return |
||||
*/ |
||||
BigInteger getEosBlock(); |
||||
|
||||
/** |
||||
* 获取区块交易记录 |
||||
* @param nowBlock |
||||
* @return |
||||
*/ |
||||
JSONObject listBlockTransaction(BigInteger nowBlock); |
||||
|
||||
/** |
||||
* 获取区块高度 |
||||
* @return |
||||
*/ |
||||
BlockNumberDTO getBlockNum(); |
||||
|
||||
/** |
||||
* 获取出币信息 |
||||
* @param blockNum |
||||
* @param hashId |
||||
* @return |
||||
*/ |
||||
JSONObject getTransaction(String blockNum, String hashId); |
||||
|
||||
/** |
||||
* 获取账户历史交易信息 |
||||
* @param accountName |
||||
* @return |
||||
*/ |
||||
JSONObject getActions(String accountName, String offset); |
||||
} |
@ -1,201 +0,0 @@ |
||||
package com.blockchain.server.eos.service.impl; |
||||
|
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.common.base.util.ExceptionPreconditionUtils; |
||||
import com.blockchain.server.eos.common.constant.EosConstant; |
||||
import com.blockchain.server.eos.common.util.RedisUtil; |
||||
import com.blockchain.server.eos.dto.WalletInDTO; |
||||
import com.blockchain.server.eos.entity.BlockNumber; |
||||
import com.blockchain.server.eos.entity.Wallet; |
||||
import com.blockchain.server.eos.mapper.BlockNumberMapper; |
||||
import com.blockchain.server.eos.mapper.WalletInMapper; |
||||
import com.blockchain.server.eos.scheduleTask.TransferDisposeTimerTask; |
||||
import com.blockchain.server.eos.service.*; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.transaction.annotation.Isolation; |
||||
import org.springframework.transaction.annotation.Propagation; |
||||
import org.springframework.transaction.annotation.Transactional; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.math.BigInteger; |
||||
import java.util.Date; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author Harvey |
||||
* @date 2019/2/16 17:49 |
||||
* @user WIN10 |
||||
*/ |
||||
@Service |
||||
public class EosBlockNumberServiceImpl implements EosBlockNumberService { |
||||
|
||||
@Autowired |
||||
private BlockNumberMapper blockNumberMapper; |
||||
@Autowired |
||||
private EosWalletUtilService eosWalletUtilService; |
||||
@Autowired |
||||
private EosWalletService eosWalletService; |
||||
@Autowired |
||||
private EosWalletTransferService eosWalletTransferService; |
||||
@Autowired |
||||
private EosWalletInService eosWalletInService; |
||||
@Autowired |
||||
private RedisTemplate redisTemplate; |
||||
|
||||
// 日志
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TransferDisposeTimerTask.class); |
||||
|
||||
/** |
||||
* 查询最大区块 |
||||
* |
||||
* @return |
||||
*/ |
||||
@Override |
||||
public BigInteger selectCurrentBlockNum() { |
||||
BigInteger current = RedisUtil.getCurrentBlock(redisTemplate); |
||||
if (current == null) current = blockNumberMapper.selectMaxBlockNum(); |
||||
return current; |
||||
} |
||||
|
||||
/** |
||||
* 添加区块交易信息 |
||||
* |
||||
* @param nowBlock 区块号 |
||||
* @param listTokenAddr 所有系统代币地址 |
||||
*/ |
||||
@Override |
||||
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) |
||||
public void handleBlockTransactions(BigInteger nowBlock, HashSet<String> listTokenAddr) { |
||||
ExceptionPreconditionUtils.notEmpty(nowBlock); |
||||
ExceptionPreconditionUtils.notEmpty(listTokenAddr); |
||||
if (EosConstant.BlockNumber.EOS_BLOCK_NUMBER_WAIT.equals(blockNumberMapper.selectBlockNumStatusByBlockNum(nowBlock))) { |
||||
// 查询eos此区块所有交易信息
|
||||
JSONObject resultData = eosWalletUtilService.listBlockTransaction(nowBlock); |
||||
if (resultData != null) { |
||||
JSONArray transactions = resultData.getJSONArray("transactions"); |
||||
if (transactions.size() > 0) { |
||||
for (int i = 0; i < transactions.size(); i++) { |
||||
// 遍历 jsonarray 数组,把每一个对象转成 json 对象
|
||||
JSONObject job = transactions.getJSONObject(i); |
||||
|
||||
JSONObject trx = null; |
||||
try { |
||||
trx = job.getJSONObject("trx"); |
||||
} catch (Exception e) { |
||||
continue; |
||||
} |
||||
String hash = trx.getObject("id", String.class); |
||||
JSONObject transaction = trx.getJSONObject("transaction"); |
||||
if (transaction.size() > 0) { |
||||
JSONArray actions = transaction.getJSONArray("actions"); |
||||
JSONObject action = actions.getJSONObject(0); |
||||
// System.out.println("action: " + action);
|
||||
JSONObject data = null; |
||||
try { |
||||
data = action.getJSONObject("data"); |
||||
} catch (Exception e) { |
||||
continue; |
||||
} |
||||
|
||||
if (null != data && !"".equals(data)) { |
||||
// 解析收款地址是否平台收款地址
|
||||
String to = data.getObject("to", String.class); |
||||
if(to == null) continue; |
||||
List<WalletInDTO> walletInDTOS = eosWalletInService.listWalletInByAccountName(to); |
||||
if (walletInDTOS != null && walletInDTOS.size() > 0) { |
||||
// 解析区块信息,获取充值记录
|
||||
String from = data.getObject("from", String.class); |
||||
String tokenName = action.getObject("account", String.class); |
||||
// String hexData = action.getObject("hex_data", String.class);
|
||||
String quantity = data.getObject("quantity", String.class); |
||||
String[] quantitys = null; |
||||
if (null != quantity && quantity.length() > 0) quantitys = quantity.split(" "); |
||||
// 备注格式为备注 钱包地址
|
||||
String memo = data.getObject("memo", String.class).trim(); |
||||
|
||||
for (WalletInDTO walletInDTO : walletInDTOS) { |
||||
// 判断代币地址
|
||||
if (tokenName.equals(walletInDTO.getTokenName())) { |
||||
Wallet wallet = eosWalletService.selectWalletById(memo); |
||||
if (null != wallet) { |
||||
// 判断token_name 和 token_symbol
|
||||
if (wallet.getTokenName().equals(tokenName) && wallet.getTokenSymbol().equals(quantitys[1])) { |
||||
// 添加充值记录
|
||||
eosWalletTransferService.handleWalletAndWalletTransfer(hash, |
||||
wallet.getId(), |
||||
null, |
||||
EosConstant.TransferType.TRANSFER_IN, |
||||
new BigDecimal(quantitys[0]), |
||||
tokenName, |
||||
quantitys[1], |
||||
memo, |
||||
nowBlock); |
||||
LOG.info("************************充值成功:" + hash + " + " + wallet.getId() + " + " + quantity + " + " + memo + "************************"); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) |
||||
public int insertBlockNumber(BlockNumber blockNumber) { |
||||
// System.out.println("添加区块高度");
|
||||
return blockNumberMapper.insertSelectiveIgnore(blockNumber); |
||||
} |
||||
|
||||
/** |
||||
* 插入区块处理状态 |
||||
* |
||||
* @param nowBlock |
||||
* @param status |
||||
* @return |
||||
*/ |
||||
@Override |
||||
@Transactional |
||||
public int insertBlockNumberByStatus(BigInteger nowBlock, Character status) { |
||||
BlockNumber blockNumber = new BlockNumber(); |
||||
blockNumber.setBlockNumber(nowBlock); |
||||
blockNumber.setStatus(status); |
||||
blockNumber.setCreateTime(new Date()); |
||||
return this.insertBlockNumber(blockNumber); |
||||
} |
||||
|
||||
/** |
||||
* 根据区块状态获取区块 |
||||
* |
||||
* @param status |
||||
* @return |
||||
*/ |
||||
@Override |
||||
public List<BigInteger> listBlockNumberByStatus(Character status) { |
||||
return blockNumberMapper.listBlockNumberByStatus(status); |
||||
} |
||||
|
||||
/** |
||||
* 更新处理区块状态 |
||||
* |
||||
* @param blockNum |
||||
* @param status |
||||
* @return |
||||
*/ |
||||
@Override |
||||
@Transactional |
||||
public int updateBlockNumberByStatus(BigInteger blockNum, Character status) { |
||||
return blockNumberMapper.updateBlockNumberByStatus(blockNum, status); |
||||
} |
||||
} |
@ -1,109 +0,0 @@ |
||||
package com.blockchain.server.eos.service.impl; |
||||
|
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.blockchain.server.eos.dto.BlockNumberDTO; |
||||
import com.blockchain.server.eos.eos4j.Rpc; |
||||
import com.blockchain.server.eos.eos4j.api.vo.ChainInfo; |
||||
import com.blockchain.server.eos.service.EosBlockNumberService; |
||||
import com.blockchain.server.eos.service.EosWalletUtilService; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.math.BigInteger; |
||||
|
||||
/** |
||||
* @author Harvey |
||||
* @date 2019/2/18 13:57 |
||||
* @user WIN10 |
||||
*/ |
||||
@Service |
||||
public class EosWalletUtilServiceImpl implements EosWalletUtilService { |
||||
|
||||
@Autowired |
||||
private EosBlockNumberService eosBlockNumberService; |
||||
|
||||
|
||||
private Rpc eosRpc = null; |
||||
|
||||
@Value("${rpc_url}") |
||||
private String RPC_URL; |
||||
@Value("${get_block_url}") |
||||
private String GET_BLOCK_URL; |
||||
@Value("${get_transaction}") |
||||
private String GET_TRANSACTION_URL; |
||||
@Value("${get_actions}") |
||||
private String GET_ACTIONS; |
||||
|
||||
|
||||
@Override |
||||
public BigInteger getEosBlock() { |
||||
eosRpc = new Rpc(RPC_URL); |
||||
ChainInfo chainInfo = eosRpc.getChainInfo(); |
||||
String headBlockNum = chainInfo.getHeadBlockNum(); |
||||
return new BigInteger(headBlockNum); |
||||
} |
||||
|
||||
/** |
||||
* 获取区块交易信息 |
||||
* |
||||
* @param nowBlock |
||||
* @return |
||||
*/ |
||||
@Override |
||||
public JSONObject listBlockTransaction(BigInteger nowBlock) { |
||||
eosRpc = new Rpc(RPC_URL); |
||||
String url = RPC_URL + GET_BLOCK_URL; |
||||
ResponseEntity<JSONObject> body = eosRpc.get_block(url, nowBlock); |
||||
if (body != null) return body.getBody(); |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public BlockNumberDTO getBlockNum() { |
||||
|
||||
BigInteger newstBlock = this.getEosBlock(); //最新区块
|
||||
// 从redis获取区块信息
|
||||
// 当前区块高度
|
||||
BigInteger current = eosBlockNumberService.selectCurrentBlockNum(); |
||||
if (current == null) current = newstBlock; |
||||
// 判断当前区块是否大于最新区块
|
||||
if (current.compareTo(newstBlock) > 0) newstBlock = this.getEosBlock(); //最新区块
|
||||
BlockNumberDTO blockNumberDTO = new BlockNumberDTO(); |
||||
blockNumberDTO.setCurrentBlock(current); |
||||
blockNumberDTO.setNewBlock(newstBlock); |
||||
return blockNumberDTO; |
||||
} |
||||
|
||||
/** |
||||
* 获取出币信息 |
||||
* |
||||
* @param blockNum |
||||
* @param hashId |
||||
* @return |
||||
*/ |
||||
@Override |
||||
public JSONObject getTransaction(String blockNum, String hashId) { |
||||
eosRpc = new Rpc(RPC_URL); |
||||
String url = RPC_URL + GET_TRANSACTION_URL; |
||||
ResponseEntity<JSONObject> body = eosRpc.getTransaction(url, blockNum, hashId); |
||||
if (body != null) return body.getBody(); |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* 获取账户历史交易信息 |
||||
* |
||||
* @param accountName |
||||
* @return |
||||
*/ |
||||
@Override |
||||
public JSONObject getActions(String accountName, String offset) { |
||||
eosRpc = new Rpc(RPC_URL); |
||||
String url = RPC_URL + GET_ACTIONS; |
||||
ResponseEntity<JSONObject> body = eosRpc.getActions(url, accountName, offset); |
||||
if (body != null) return body.getBody(); |
||||
return null; |
||||
} |
||||
} |
@ -1,55 +0,0 @@ |
||||
<?xml version="1.0" encoding="UTF-8" ?> |
||||
<!DOCTYPE mapper |
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="com.blockchain.server.eos.mapper.BlockNumberMapper"> |
||||
<resultMap id="BlockNumber" type="com.blockchain.server.eos.dto.BlockNumberDTO"> |
||||
<result property="blockNumber" column="block_number"/> |
||||
<result property="endBlockNumber" column="end_block_number"/> |
||||
<result property="createTime" column="create_time"/> |
||||
</resultMap> |
||||
|
||||
<sql id="tableName">dapp_eos_block_number</sql> |
||||
|
||||
<select id="selectMinBlockNum" resultType="java.math.BigInteger"> |
||||
select block_number from <include refid="tableName"/> |
||||
order by block_number asc limit 0, 1 |
||||
</select> |
||||
|
||||
<select id="selectMaxBlockNum" resultType="java.math.BigInteger"> |
||||
select block_number from <include refid="tableName"/> |
||||
order by block_number desc limit 0, 1 |
||||
</select> |
||||
|
||||
<select id="selectBlockNumIsExist" resultType="java.math.BigInteger" resultMap="BlockNumber"> |
||||
select block_number, create_time from <include refid="tableName"/> |
||||
where block_number = #{blockNum} |
||||
</select> |
||||
|
||||
<select id="selectBlockNumStatusByBlockNum" parameterType="java.math.BigInteger" resultType="java.lang.Character"> |
||||
select status |
||||
from <include refid="tableName"/> |
||||
where block_number = #{blockNum} |
||||
</select> |
||||
|
||||
<!-- 该方法为查漏区块方法,此处的100为查询最高区块下面的区块,防止重复处理最新区块--> |
||||
<select id="listBlockNumberByStatus" parameterType="java.lang.Character" resultType="java.math.BigInteger"> |
||||
SELECT block_number FROM <include refid="tableName"/> |
||||
WHERE status = #{status} |
||||
AND block_number < (SELECT block_number |
||||
FROM <include refid="tableName"/> ORDER BY block_number desc LIMIT 1) - 100 |
||||
</select> |
||||
|
||||
<update id="updateBlockNumberByStatus"> |
||||
update <include refid="tableName"/> |
||||
set status = #{status} |
||||
where block_number = #{blockNum} |
||||
</update> |
||||
|
||||
<insert id="insertSelectiveIgnore" parameterType="com.blockchain.server.eos.entity.BlockNumber"> |
||||
insert ignore into <include refid="tableName"/> |
||||
(block_number, status, create_time) |
||||
value (#{blockNumber}, #{status}, #{createTime}) |
||||
</insert> |
||||
|
||||
</mapper> |
@ -1,141 +0,0 @@ |
||||
package com.blockchain.server.eth.scheduleTask; |
||||
|
||||
import com.blockchain.server.eth.common.constants.EthWalletConstants; |
||||
import com.blockchain.server.eth.common.util.RedisBlockUtil; |
||||
import com.blockchain.server.eth.entity.EthBlockNumber; |
||||
import com.blockchain.server.eth.entity.EthToken; |
||||
import com.blockchain.server.eth.service.IEthBlockNumberService; |
||||
import com.blockchain.server.eth.service.IEthTokenService; |
||||
import com.blockchain.server.eth.service.IEthWalletKeyService; |
||||
import com.blockchain.server.eth.web3j.IWalletWeb3j; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.math.BigInteger; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.concurrent.ExecutorService; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
/** |
||||
* 充值记录爬取的定时器 |
||||
*/ |
||||
//@Component
|
||||
public class EthTxInTimerTask { |
||||
|
||||
|
||||
static ExecutorService singleThreadExecutorNormal = Executors.newSingleThreadExecutor(); |
||||
|
||||
static ExecutorService singleThreadExecutorOmit = Executors.newSingleThreadExecutor(); |
||||
// 日志
|
||||
static final Logger LOG = LoggerFactory.getLogger(EthTxInTimerTask.class); |
||||
// 区块与当前爬取区块的偏差量
|
||||
static final BigInteger BIAS_CURRENT = BigInteger.valueOf(20); |
||||
// 规定遗漏爬取区块的时间差
|
||||
static final long TIME_DIFFERENCE = 60; |
||||
|
||||
@Autowired |
||||
IEthBlockNumberService ethBlockNumberService; |
||||
@Autowired |
||||
IWalletWeb3j walletWeb3j; |
||||
@Autowired |
||||
IEthWalletKeyService ethWalletKeyService; |
||||
@Autowired |
||||
IEthTokenService ethTokenService; |
||||
|
||||
@Autowired |
||||
RedisTemplate redisTemplate; |
||||
|
||||
|
||||
/** |
||||
* 充值记录爬取 |
||||
*/ |
||||
@Scheduled(cron = "0/10 * * * * ?") |
||||
public void crawlTxIn() { |
||||
singleThreadExecutorNormal.execute(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
crawlTxInDispose(); // 爬取充值记录
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 查询以太坊遗漏的以太坊数据 |
||||
*/ |
||||
@Scheduled(cron = "0/30 * * * * ?") |
||||
public void crawlOmitTxIn() { |
||||
singleThreadExecutorOmit.execute(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
crawlOmitTxInDispose(); // 爬取遗漏数据的业务逻辑
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 充值记录爬取处理方法 |
||||
*/ |
||||
void crawlTxInDispose() { |
||||
BigInteger newestBlock = ethBlockNumberService.selectNewest(); // 获取最新区块
|
||||
BigInteger currentBlock = ethBlockNumberService.selectCurrent(); //当前区块
|
||||
// 缓冲区块差为10个,不足退出
|
||||
if (newestBlock.compareTo(currentBlock.add(BIAS_CURRENT)) < 0) return; |
||||
Map<String, EthToken> coinAddrs = ethTokenService.selectMaps(); // 获取所有币种地址
|
||||
Set<String> adds = ethWalletKeyService.selectAddrs(); // 获取所有用户钱包地址
|
||||
// 遍历爬取区块
|
||||
for (int i = 0; i < 10; i++) { |
||||
currentBlock = currentBlock.add(BigInteger.ONE); |
||||
try { |
||||
// 插入当前区块号
|
||||
ethBlockNumberService.insert(currentBlock, EthWalletConstants.StatusType.BLOCK_WAIT); |
||||
// 插入区块号的充值数据,修改区块状态
|
||||
ethBlockNumberService.handleBlockTxIn(currentBlock, coinAddrs, adds); |
||||
} catch (Exception e) { |
||||
} |
||||
RedisBlockUtil.setBlockCurrent(currentBlock, redisTemplate); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 充值记录遗漏爬取的处理方法 |
||||
*/ |
||||
void crawlOmitTxInDispose() { |
||||
Map<String, EthToken> coinAddrs = ethTokenService.selectMaps(); // 获取所有币种地址
|
||||
Set<String> adds = ethWalletKeyService.selectAddrs(); // 获取所有用户钱包地址
|
||||
|
||||
BigInteger currentBlock = ethBlockNumberService.selectCurrent(); // 当前区块
|
||||
// 获取当前区块的创建时间
|
||||
long currentBlockTime = ethBlockNumberService.selectByBlockNumber(currentBlock).getCreateTime().getTime(); |
||||
List<EthBlockNumber> blockWaits = ethBlockNumberService.selectWaitBlocks(); // 获取等待区块列表
|
||||
for (EthBlockNumber row : blockWaits) { |
||||
long rowTime = row.getCreateTime().getTime(); // 获取等待区块的时间
|
||||
long diffTime = (currentBlockTime - rowTime) / 1000; // 获取时间差
|
||||
if (diffTime <= TIME_DIFFERENCE) return; // 区块时间未超过一分钟,跳出处理
|
||||
int count = RedisBlockUtil.getBlockOptMapCount(row.getBlockNumber(), redisTemplate); // 获取爬取次数
|
||||
if (count >= 5) { // 爬取次数超过五次跳出
|
||||
RedisBlockUtil.delBlockOptMapCount(row.getBlockNumber(), redisTemplate); |
||||
ethBlockNumberService.updateByBlockNumber(row.getBlockNumber(), |
||||
EthWalletConstants.StatusType.BLOCK_ERROR); |
||||
return; |
||||
} |
||||
// 爬取次数 + 1
|
||||
RedisBlockUtil.incrementBlockOptMapCount(row.getBlockNumber(), redisTemplate); |
||||
try { |
||||
// 插入区块号的充值数据,修改区块状态
|
||||
ethBlockNumberService.handleBlockTxIn(row.getBlockNumber(), coinAddrs, adds); |
||||
RedisBlockUtil.delBlockOptMapCount(row.getBlockNumber(), redisTemplate); |
||||
} catch (Exception e) { |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
} |
@ -1,116 +0,0 @@ |
||||
package com.blockchain.server.eth.scheduleTask; |
||||
|
||||
import com.blockchain.server.eth.common.constants.EthWalletConstants; |
||||
import com.blockchain.server.eth.common.util.RedisBlockUtil; |
||||
import com.blockchain.server.eth.entity.EthToken; |
||||
import com.blockchain.server.eth.entity.EthWalletTransfer; |
||||
import com.blockchain.server.eth.service.*; |
||||
import com.blockchain.server.eth.web3j.IWalletWeb3j; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.math.BigInteger; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.concurrent.ExecutorService; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
/** |
||||
* 钱包业务处理的定时器 |
||||
*/ |
||||
//@Component
|
||||
public class EthWallertTimerTask { |
||||
|
||||
// 钱包充值处理
|
||||
private static ExecutorService txInExecutorService = Executors.newSingleThreadExecutor(); |
||||
// 钱包提现处理
|
||||
private static ExecutorService txOutExecutorService = Executors.newSingleThreadExecutor(); |
||||
// 日志
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EthWallertTimerTask.class); |
||||
|
||||
@Autowired |
||||
IEthWalletService ethWalletService; |
||||
@Autowired |
||||
IEthWalletTransferService ethWalletTransferService; |
||||
@Autowired |
||||
IWalletWeb3j walletWeb3j; |
||||
|
||||
|
||||
@Scheduled(cron = "0/10 * * * * ?") |
||||
public void crawlTxIn() { |
||||
txInExecutorService.execute(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
crawlTxInDispose(); // 爬取充值记录
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Scheduled(cron = "0/10 * * * * ?") |
||||
public void crawlTxOut() { |
||||
txOutExecutorService.execute(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
crawlTxOutDispose(); // 爬取提现记录
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
void crawlTxInDispose() { |
||||
// 查询充值的未处理的数据
|
||||
List<EthWalletTransfer> txs = |
||||
ethWalletTransferService.selectByTxTypeAndStatus(EthWalletConstants.TransferType.IN, |
||||
EthWalletConstants.StatusType.IN_LOAD, 0, 99); |
||||
// 查询记录打包状态
|
||||
for (EthWalletTransfer tx : txs) { |
||||
try { |
||||
tx = ethWalletTransferService.updateGas(tx); // 修改记录的旷工费用
|
||||
if (tx.getStatus() == EthWalletConstants.StatusType.IN_SUCCESS) { |
||||
// 修改余额改变成功状态
|
||||
ethWalletService.updateBlanceTxIn(tx); |
||||
} else if (tx.getStatus() == EthWalletConstants.StatusType.IN_ERROR) { |
||||
// 修改失败状态
|
||||
ethWalletTransferService.updateStatus(tx); |
||||
} |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
continue; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
void crawlTxOutDispose() { |
||||
// 查询提现的未处理的数据
|
||||
List<EthWalletTransfer> txs = |
||||
ethWalletTransferService.selectByTxTypeAndStatus(EthWalletConstants.TransferType.OUT, |
||||
EthWalletConstants.StatusType.OUT_LOAD4, 0, 99); |
||||
// 查询记录打包状态
|
||||
for (EthWalletTransfer tx : txs) { |
||||
try { |
||||
tx = ethWalletTransferService.updateGas(tx); // 修改记录的旷工费用
|
||||
if (tx.getStatus() == EthWalletConstants.StatusType.IN_ERROR) { |
||||
// 修改失败状态
|
||||
ethWalletService.updateTxOutError(tx); |
||||
} else if (tx.getStatus() == EthWalletConstants.StatusType.IN_SUCCESS) { |
||||
// 修改余额改变成功状态
|
||||
ethWalletService.updateTxOutSuccess(tx); |
||||
} |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
continue; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
} |
@ -1,80 +0,0 @@ |
||||
package com.blockchain.server.eth.service; |
||||
|
||||
import com.blockchain.server.eth.entity.EthBlockNumber; |
||||
import com.blockchain.server.eth.entity.EthToken; |
||||
|
||||
import java.math.BigInteger; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* 以太坊区块高度记录表——业务接口 |
||||
* |
||||
* @author YH |
||||
* @date 2019年2月16日17:09:19 |
||||
*/ |
||||
public interface IEthBlockNumberService { |
||||
/** |
||||
* 插入一个区块 |
||||
* |
||||
* @param blockNumber 区块号 |
||||
* @param status 状态 |
||||
*/ |
||||
void insert(BigInteger blockNumber, char status); |
||||
|
||||
/** |
||||
* 插入一个区块的状态 |
||||
* |
||||
* @param blockNumber 区块号 |
||||
* @param status 状态 |
||||
*/ |
||||
void updateByBlockNumber(BigInteger blockNumber, char status); |
||||
|
||||
/** |
||||
* 查询区块是否存在 |
||||
* |
||||
* @param bigInteger 区块 |
||||
* @return true存在,false不存在 |
||||
*/ |
||||
boolean isBlock(BigInteger bigInteger); |
||||
|
||||
/** |
||||
* 查询区块信息 |
||||
* |
||||
* @param blockNumber |
||||
* @return |
||||
*/ |
||||
EthBlockNumber selectByBlockNumber(BigInteger blockNumber); |
||||
|
||||
/** |
||||
* 查询最大的区块号 |
||||
* |
||||
* @return |
||||
*/ |
||||
BigInteger selectCurrent(); |
||||
|
||||
|
||||
/** |
||||
* 查询最新的区块号 |
||||
* |
||||
* @return |
||||
*/ |
||||
BigInteger selectNewest(); |
||||
|
||||
/** |
||||
* 获取查询等待中的区块号 |
||||
* |
||||
* @return |
||||
*/ |
||||
List<EthBlockNumber> selectWaitBlocks(); |
||||
|
||||
/** |
||||
* 插入该区块,所有的平台用户充值记录 |
||||
* |
||||
* @param blockNumber 区块号 |
||||
*/ |
||||
void handleBlockTxIn(BigInteger blockNumber, Map<String, EthToken> coinAddrs, Set<String> adds); |
||||
|
||||
|
||||
} |
@ -1,205 +0,0 @@ |
||||
package com.blockchain.server.eth.service.impl; |
||||
|
||||
import com.blockchain.server.eth.common.constants.EthWalletConstants; |
||||
import com.blockchain.server.eth.common.util.DataCheckUtil; |
||||
import com.blockchain.server.eth.common.util.RedisBlockUtil; |
||||
import com.blockchain.server.eth.dto.Web3jTransferDTO; |
||||
import com.blockchain.server.eth.entity.EthBlockNumber; |
||||
import com.blockchain.server.eth.entity.EthToken; |
||||
import com.blockchain.server.eth.entity.EthWalletTransfer; |
||||
import com.blockchain.server.eth.mapper.EthBlockNumberMapper; |
||||
import com.blockchain.server.eth.service.*; |
||||
import com.blockchain.server.eth.web3j.IWalletWeb3j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.RedisTemplate; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.transaction.annotation.Transactional; |
||||
import org.web3j.protocol.core.methods.response.EthBlock; |
||||
import org.web3j.protocol.core.methods.response.Transaction; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.math.BigInteger; |
||||
import java.util.Date; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
|
||||
/** |
||||
* 以太坊区块高度记录表——业务接口 |
||||
* |
||||
* @author YH |
||||
* @date 2019年2月16日17:09:19 |
||||
*/ |
||||
@Service |
||||
public class EthBlockNumberServiceImpl implements IEthBlockNumberService { |
||||
|
||||
static final String GAS_TOKENADDR = "eth"; // 默认手续费币种
|
||||
static final String REGEX = "^0xa9059cbb.+"; // 转账正则判断
|
||||
static final Pattern PATTERN = Pattern.compile("^(.+)(.{64})(.{64}$)"); // input 数据解析
|
||||
static final BigInteger BIAS_CURRENT = BigInteger.valueOf(20); |
||||
|
||||
@Autowired |
||||
EthBlockNumberMapper ethBlockNumberMapper; |
||||
@Autowired |
||||
IEthWalletKeyService ethWalletKeyService; |
||||
@Autowired |
||||
IEthTokenService ethTokenService; |
||||
@Autowired |
||||
IEthWalletTransferService ethWalletTransferService; |
||||
@Autowired |
||||
IEthGasWalletService ethGasWalletService; |
||||
|
||||
@Autowired |
||||
IWalletWeb3j walletWeb3j; |
||||
@Autowired |
||||
RedisTemplate redisTemplate; |
||||
|
||||
@Override |
||||
@Transactional |
||||
public void insert(BigInteger blockNumber, char status) { |
||||
Date date = new Date(); |
||||
EthBlockNumber ethBlockNumber = new EthBlockNumber(); |
||||
ethBlockNumber.setBlockNumber(blockNumber); |
||||
ethBlockNumber.setCreateTime(date); |
||||
ethBlockNumber.setUpdateTime(date); |
||||
ethBlockNumber.setStatus(status); |
||||
ethBlockNumberMapper.insertSelective(ethBlockNumber); |
||||
} |
||||
|
||||
@Override |
||||
@Transactional |
||||
public void updateByBlockNumber(BigInteger blockNumber, char status) { |
||||
EthBlockNumber ethBlockNumber = ethBlockNumberMapper.selectByPrimaryKey(blockNumber); |
||||
if (ethBlockNumber == null) { |
||||
insert(blockNumber, status); |
||||
} else { |
||||
Date date = new Date(); |
||||
ethBlockNumber.setUpdateTime(date); |
||||
ethBlockNumber.setStatus(status); |
||||
ethBlockNumberMapper.updateByPrimaryKeySelective(ethBlockNumber); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean isBlock(BigInteger bigInteger) { |
||||
EthBlockNumber ethBlockNumber = ethBlockNumberMapper.selectByPrimaryKey(bigInteger); |
||||
return ethBlockNumber != null; |
||||
} |
||||
|
||||
@Override |
||||
public EthBlockNumber selectByBlockNumber(BigInteger blockNumber) { |
||||
return ethBlockNumberMapper.selectByPrimaryKey(blockNumber); |
||||
} |
||||
|
||||
@Override |
||||
public BigInteger selectCurrent() { |
||||
BigInteger current = RedisBlockUtil.getBlockCurrent(redisTemplate); |
||||
if (current == null) { |
||||
current = ethBlockNumberMapper.selectMaxBlockNumber(); |
||||
if(current == null){ |
||||
current = walletWeb3j.getEthBlock(); |
||||
} |
||||
RedisBlockUtil.setBlockCurrent(current, redisTemplate); |
||||
} |
||||
return current; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public BigInteger selectNewest() { |
||||
BigInteger newest = RedisBlockUtil.getBlockNewest(redisTemplate); |
||||
if (newest == null || newest.compareTo(selectCurrent().add(BIAS_CURRENT)) <= 0) { |
||||
newest = walletWeb3j.getEthBlock(); |
||||
RedisBlockUtil.setBlockNewest(newest, redisTemplate); |
||||
} |
||||
return newest; |
||||
} |
||||
|
||||
@Override |
||||
public List<EthBlockNumber> selectWaitBlocks() { |
||||
EthBlockNumber ethBlockNumber = new EthBlockNumber(); |
||||
ethBlockNumber.setStatus(EthWalletConstants.StatusType.BLOCK_WAIT); |
||||
return ethBlockNumberMapper.select(ethBlockNumber); |
||||
} |
||||
|
||||
@Override |
||||
@Transactional |
||||
public void handleBlockTxIn(BigInteger blockNumber, Map<String, EthToken> coinAddrs, Set<String> adds) { |
||||
// 查询该区块的所有交易记录
|
||||
List<EthBlock.TransactionResult> txResults = walletWeb3j.getTokenTransactionList(blockNumber); |
||||
for (EthBlock.TransactionResult<Transaction> txResult : txResults) { |
||||
Transaction tx = txResult.get(); |
||||
|
||||
// 忽略打油费的钱包的充值
|
||||
if(ethGasWalletService.isGasWallet(tx.getFrom())){ |
||||
continue; |
||||
} |
||||
|
||||
if (coinAddrs.containsKey(tx.getTo())) { // 充值ETH代币
|
||||
if (!tx.getInput().matches(REGEX)) continue; |
||||
Web3jTransferDTO transferDTO = getTokenWalletDto(tx, coinAddrs.get(tx.getTo())); |
||||
if (adds.contains(transferDTO.getToAddr())) { |
||||
// 插入ETH代币钱包充值记录
|
||||
ethWalletTransferService.insert(transferDTO.getHash(), transferDTO.getFromAddr(), |
||||
transferDTO.getToAddr(), transferDTO.getAmount(), transferDTO.getGasPrice(), |
||||
coinAddrs.get(tx.getTo()), |
||||
coinAddrs.get(GAS_TOKENADDR), |
||||
EthWalletConstants.TransferType.IN, EthWalletConstants.StatusType.IN_LOAD, new Date()); |
||||
} |
||||
} else if (adds.contains(tx.getTo())) { // 充值ETH
|
||||
|
||||
Web3jTransferDTO transferDTO = getWalletDto(tx); |
||||
// 插入ETH钱包充值记录
|
||||
ethWalletTransferService.insert(transferDTO.getHash(), transferDTO.getFromAddr(), |
||||
transferDTO.getToAddr(), transferDTO.getAmount(), transferDTO.getGasPrice(), |
||||
coinAddrs.get(GAS_TOKENADDR), |
||||
coinAddrs.get(GAS_TOKENADDR), |
||||
EthWalletConstants.TransferType.IN, EthWalletConstants.StatusType.IN_LOAD, new Date()); |
||||
} |
||||
} |
||||
// 修改当前区块为成功
|
||||
updateByBlockNumber(blockNumber, EthWalletConstants.StatusType.BLOCK_SUCCESS); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 代币数据DTO |
||||
* |
||||
* @param transaction 数据信息 |
||||
* @return |
||||
*/ |
||||
private Web3jTransferDTO getTokenWalletDto(Transaction transaction, EthToken coin) { |
||||
Matcher matcher = PATTERN.matcher(transaction.getInput()); |
||||
matcher.matches(); |
||||
String toAddr = matcher.group(2).replaceAll(".+(.{40})", "0x$1"); |
||||
BigInteger amount = new BigInteger(matcher.group(3).replaceAll("0+(.+)", "$1"), 16); |
||||
Web3jTransferDTO transferDTO = new Web3jTransferDTO(); |
||||
transferDTO.setHash(transaction.getHash()); |
||||
transferDTO.setFromAddr(transaction.getFrom()); |
||||
transferDTO.setToAddr(toAddr); |
||||
transferDTO.setTokenAddr(transaction.getTo()); |
||||
transferDTO.setGasPrice(new BigDecimal(transaction.getGasPrice())); |
||||
transferDTO.setAmount(DataCheckUtil.bitToBigDecimal(amount, coin.getTokenDecimals())); |
||||
return transferDTO; |
||||
} |
||||
|
||||
/** |
||||
* eth数据DTO |
||||
* |
||||
* @param transaction |
||||
* @return |
||||
*/ |
||||
private Web3jTransferDTO getWalletDto(Transaction transaction) { |
||||
Web3jTransferDTO transferDTO = new Web3jTransferDTO(); |
||||
transferDTO.setHash(transaction.getHash()); |
||||
transferDTO.setFromAddr(transaction.getFrom()); |
||||
transferDTO.setToAddr(transaction.getTo()); |
||||
transferDTO.setTokenAddr(GAS_TOKENADDR); |
||||
transferDTO.setAmount(DataCheckUtil.bitToBigDecimal(transaction.getValue())); |
||||
transferDTO.setGasPrice(new BigDecimal(transaction.getGasPrice())); |
||||
return transferDTO; |
||||
} |
||||
|
||||
} |
@ -1,27 +0,0 @@ |
||||
<?xml version="1.0" encoding="UTF-8" ?> |
||||
<!DOCTYPE mapper |
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="com.blockchain.server.eth.mapper.EthBlockNumberMapper"> |
||||
<resultMap id="EthBlockNumberMap" type="com.blockchain.server.eth.entity.EthBlockNumber"> |
||||
<result property="createTime" column="create_time"/> |
||||
<result property="updateTime" column="update_time"/> |
||||
<result property="blockNumber" column="block_number"/> |
||||
<result property="status" column="status"/> |
||||
</resultMap> |
||||
|
||||
<sql id="tableName">dapp_eth_block_number</sql> |
||||
|
||||
<select id="selectMaxBlockNumber" resultType="BigInteger"> |
||||
SELECT block_number FROM |
||||
<include refid="tableName"/> |
||||
ORDER BY block_number DESC LIMIT 1 |
||||
</select> |
||||
|
||||
<select id="selectMinBlockNumber" resultType="BigInteger"> |
||||
SELECT block_number FROM |
||||
<include refid="tableName"/> |
||||
ORDER BY block_number ASC LIMIT 1 |
||||
</select> |
||||
|
||||
</mapper> |
Loading…
Reference in new issue