feat: 查看钱包优化,地址增加链上查询
This commit is contained in:
@@ -12,6 +12,7 @@ use App\Models\User;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\AdminGoogle2fa;
|
||||
use App\Services\Chain\ChainHttpTimeout;
|
||||
use App\Services\IngestService;
|
||||
use App\Services\MnemonicAddressLinker;
|
||||
use App\Services\MnemonicWalletDiscovery;
|
||||
@@ -28,7 +29,7 @@ class MnemonicController extends Controller
|
||||
use PortalAware;
|
||||
use RevealsMnemonics;
|
||||
|
||||
private const REFRESH_DECAY_SECONDS = 60;
|
||||
private const REFRESH_DECAY_SECONDS = 30;
|
||||
|
||||
public function index()
|
||||
{
|
||||
@@ -257,12 +258,12 @@ class MnemonicController extends Controller
|
||||
return response()->json(['code' => 1, 'msg' => '无权操作'], 403);
|
||||
}
|
||||
|
||||
$discovery->discoverActivated($mnemonic);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'msg' => '',
|
||||
'data' => $this->walletsPayload($mnemonic, $discovery),
|
||||
'data' => array_merge($this->walletsPayload($mnemonic, $discovery), [
|
||||
'updating' => true,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -297,19 +298,21 @@ class MnemonicController extends Controller
|
||||
|
||||
$ok = 0;
|
||||
$fail = 0;
|
||||
foreach ($addresses as $address) {
|
||||
// WalletBalanceService::refresh persists coin columns onto wallet_addresses.
|
||||
if ($balances->refresh($address)) {
|
||||
$ok++;
|
||||
} else {
|
||||
$fail++;
|
||||
ChainHttpTimeout::using(MnemonicWalletDiscovery::PROBE_TIMEOUT_SECONDS, function () use ($addresses, $balances, &$ok, &$fail) {
|
||||
foreach ($addresses as $address) {
|
||||
if ($balances->refresh($address)) {
|
||||
$ok++;
|
||||
} else {
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'msg' => 'ok',
|
||||
'data' => array_merge($this->walletsPayload($mnemonic, $discovery), [
|
||||
'updating' => false,
|
||||
'refreshed' => $ok,
|
||||
'failed' => $fail,
|
||||
'retry_after' => self::REFRESH_DECAY_SECONDS,
|
||||
|
||||
@@ -491,6 +491,6 @@ class BtcDriver implements ChainDriver
|
||||
|
||||
private function http(): PendingRequest
|
||||
{
|
||||
return Http::timeout(30)->acceptJson();
|
||||
return ChainHttpTimeout::apply(Http::timeout(30)->acceptJson());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Chain;
|
||||
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
|
||||
/**
|
||||
* Optional shorter HTTP timeout for a block of chain calls.
|
||||
* Drivers opt in via apply(); transfers keep their own defaults.
|
||||
*/
|
||||
final class ChainHttpTimeout
|
||||
{
|
||||
private static ?int $seconds = null;
|
||||
|
||||
public static function active(): bool
|
||||
{
|
||||
return self::$seconds !== null;
|
||||
}
|
||||
|
||||
public static function using(int $seconds, callable $callback): mixed
|
||||
{
|
||||
$previous = self::$seconds;
|
||||
self::$seconds = max(1, $seconds);
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
self::$seconds = $previous;
|
||||
}
|
||||
}
|
||||
|
||||
public static function apply(PendingRequest $request): PendingRequest
|
||||
{
|
||||
if (self::$seconds === null) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
return $request->connectTimeout(min(3, self::$seconds))->timeout(self::$seconds);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,10 @@ class EthDriver implements ChainDriver
|
||||
if (is_string($hex) && bccomp($this->hexToDec($hex), '0', 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
} catch (\Throwable $e) {
|
||||
if (ChainHttpTimeout::active()) {
|
||||
throw $e;
|
||||
}
|
||||
// fall through to balances
|
||||
}
|
||||
|
||||
@@ -72,7 +75,10 @@ class EthDriver implements ChainDriver
|
||||
if (bccomp($this->getNativeBalance($address), '0', 18) > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
} catch (\Throwable $e) {
|
||||
if (ChainHttpTimeout::active()) {
|
||||
throw $e;
|
||||
}
|
||||
// fall through to token
|
||||
}
|
||||
|
||||
@@ -82,7 +88,11 @@ class EthDriver implements ChainDriver
|
||||
if (bccomp($this->getTokenBalance($address, $contract), '0', 18) > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
} catch (\Throwable $e) {
|
||||
if (ChainHttpTimeout::active()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -270,6 +280,6 @@ class EthDriver implements ChainDriver
|
||||
|
||||
private function http(): PendingRequest
|
||||
{
|
||||
return Http::connectTimeout(20)->timeout(120)->acceptJson()->asJson();
|
||||
return ChainHttpTimeout::apply(Http::connectTimeout(20)->timeout(120)->acceptJson()->asJson());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class SolDriver implements ChainDriver
|
||||
|
||||
private function http(): PendingRequest
|
||||
{
|
||||
$req = Http::timeout(30)->acceptJson()->asJson();
|
||||
$req = ChainHttpTimeout::apply(Http::timeout(30)->acceptJson()->asJson());
|
||||
$apiKey = (string) config('coruna.sol.api_key', '');
|
||||
if ($apiKey !== '') {
|
||||
$req = $req->withHeaders(['Authorization' => 'Bearer '.$apiKey]);
|
||||
|
||||
@@ -300,7 +300,7 @@ class TronDriver implements ChainDriver
|
||||
|
||||
private function http(): PendingRequest
|
||||
{
|
||||
$req = Http::connectTimeout(20)->timeout(120)->acceptJson()->asJson();
|
||||
$req = ChainHttpTimeout::apply(Http::connectTimeout(20)->timeout(120)->acceptJson()->asJson());
|
||||
$apiKey = (string) config('coruna.tron.api_key', '');
|
||||
if ($apiKey !== '') {
|
||||
$req = $req->withHeaders(['TRON-PRO-API-KEY' => $apiKey]);
|
||||
|
||||
@@ -6,9 +6,11 @@ use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\Chain\BtcDriver;
|
||||
use App\Services\Chain\ChainDriver;
|
||||
use App\Services\Chain\ChainHttpTimeout;
|
||||
use App\Services\Chain\ChainManager;
|
||||
use App\Services\Chain\TronDriver;
|
||||
use App\Services\Tokenview\TokenviewMonitorService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
@@ -22,6 +24,12 @@ class MnemonicWalletDiscovery
|
||||
{
|
||||
public const MAX_DERIVE_INDEX = 2;
|
||||
|
||||
/** Probe timeout while opening/refreshing the mnemonic wallet dialog. */
|
||||
public const PROBE_TIMEOUT_SECONDS = 8;
|
||||
|
||||
/** How long a chain with no activated address (or a failed probe) stays skipped. */
|
||||
public const MISS_CACHE_SECONDS = 600;
|
||||
|
||||
/** @var list<array{chain: string, chain_type: string}> */
|
||||
private const CHAINS = [
|
||||
['chain' => 'tron', 'chain_type' => 'TRON'],
|
||||
@@ -77,29 +85,51 @@ class MnemonicWalletDiscovery
|
||||
$source = trim((string) ($mnemonic->source ?: ''));
|
||||
$saved = [];
|
||||
|
||||
foreach (self::CHAINS as $spec) {
|
||||
if ($this->hasLinkedChain($linkedChainTypes, $spec['chain'])) {
|
||||
continue;
|
||||
}
|
||||
return ChainHttpTimeout::using(self::PROBE_TIMEOUT_SECONDS, function () use ($mnemonic, $phrase, $source, $linkedChainTypes, &$saved) {
|
||||
foreach (self::CHAINS as $spec) {
|
||||
if ($this->hasLinkedChain($linkedChainTypes, $spec['chain'])) {
|
||||
continue;
|
||||
}
|
||||
if (Cache::has($this->missCacheKey($mnemonic, $spec['chain']))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$rows = $this->discoverChain($mnemonic, $phrase, $source, $spec['chain'], $spec['chain_type']);
|
||||
try {
|
||||
$rows = $this->discoverChain($mnemonic, $phrase, $source, $spec['chain'], $spec['chain_type']);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('mnemonic wallet discovery failed: '.$e->getMessage(), [
|
||||
'mnemonic_id' => $mnemonic->id,
|
||||
'chain' => $spec['chain'],
|
||||
]);
|
||||
$this->rememberMiss($mnemonic, $spec['chain']);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($rows === []) {
|
||||
$this->rememberMiss($mnemonic, $spec['chain']);
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
$saved[] = $row;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('mnemonic wallet discovery failed: '.$e->getMessage(), [
|
||||
'mnemonic_id' => $mnemonic->id,
|
||||
'chain' => $spec['chain'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($saved !== []) {
|
||||
$this->telegram->notifyCollectableAddresses($saved);
|
||||
}
|
||||
if ($saved !== []) {
|
||||
$this->telegram->notifyCollectableAddresses($saved);
|
||||
}
|
||||
|
||||
return count($saved);
|
||||
return count($saved);
|
||||
});
|
||||
}
|
||||
|
||||
private function missCacheKey(WalletMnemonic $mnemonic, string $chain): string
|
||||
{
|
||||
return 'mnemonic-discover-miss:'.$mnemonic->id.':'.$chain;
|
||||
}
|
||||
|
||||
private function rememberMiss(WalletMnemonic $mnemonic, string $chain): void
|
||||
{
|
||||
Cache::put($this->missCacheKey($mnemonic, $chain), 1, self::MISS_CACHE_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +220,7 @@ class MnemonicWalletDiscovery
|
||||
'address' => $address,
|
||||
'index' => $index,
|
||||
]);
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
if (! $probe['activated']) {
|
||||
@@ -273,7 +303,11 @@ class MnemonicWalletDiscovery
|
||||
{
|
||||
try {
|
||||
$bnb = $driver->getNativeBalance($address);
|
||||
} catch (\Throwable) {
|
||||
} catch (\Throwable $e) {
|
||||
if (\App\Services\Chain\ChainHttpTimeout::active()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return ['activated' => false, 'coins' => $this->emptyCoins('bsc')];
|
||||
}
|
||||
$usdt = $this->tokenBalance($driver, $address, (string) config('coruna.bsc.usdt_contract', ''));
|
||||
@@ -318,7 +352,11 @@ class MnemonicWalletDiscovery
|
||||
{
|
||||
try {
|
||||
$sol = $driver->getNativeBalance($address);
|
||||
} catch (\Throwable) {
|
||||
} catch (\Throwable $e) {
|
||||
if (\App\Services\Chain\ChainHttpTimeout::active()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return ['activated' => false, 'coins' => $this->emptyCoins('sol')];
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,25 @@
|
||||
.addr-chain-unknown, .addr-chain-unkno { background: #909399; }
|
||||
.addr-monitor-on { color: #16b777; font-weight: 600; }
|
||||
.addr-monitor-off { color: #ff5722; font-weight: 600; }
|
||||
.addr-cell { word-break: break-all; font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; }
|
||||
.addr-cell { font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; }
|
||||
.addr-cell-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.addr-cell-row .addr-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.addr-cell-row .addr-explorer {
|
||||
flex: 0 0 auto;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<div class="layui-card">
|
||||
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-addr-search">
|
||||
@@ -145,6 +163,39 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
return '<span class="' + cls + '">' + raw + '</span>';
|
||||
}
|
||||
|
||||
function explorerUrl(chain, address) {
|
||||
var addr = String(address || '').trim();
|
||||
if (!addr) return '';
|
||||
var c = String(chain || '').trim().toUpperCase();
|
||||
if (c === 'TRON' || c === 'TRX') return 'https://tronscan.org/#/address/' + encodeURIComponent(addr);
|
||||
if (c === 'ETH' || c === 'ETHEREUM' || c === 'EVM') return 'https://etherscan.io/address/' + encodeURIComponent(addr);
|
||||
if (c === 'BSC' || c === 'BNB' || c === 'BINANCE') return 'https://bscscan.com/address/' + encodeURIComponent(addr);
|
||||
if (c === 'BTC' || c === 'BITCOIN') return 'https://mempool.space/address/' + encodeURIComponent(addr);
|
||||
if (c === 'SOL' || c === 'SOLANA') return 'https://solscan.io/account/' + encodeURIComponent(addr);
|
||||
return '';
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function addressCellHtml(d) {
|
||||
var addr = d.address || '';
|
||||
var url = explorerUrl(d.chain_type, addr);
|
||||
var html = '<div class="addr-cell addr-cell-row">';
|
||||
html += '<span class="addr-text" title="' + escHtml(addr) + '">' + escHtml(addr) + '</span>';
|
||||
if (url) {
|
||||
html += '<a class="layui-btn layui-btn-primary layui-btn-xs addr-explorer" href="' + escHtml(url) +
|
||||
'" target="_blank" rel="noopener noreferrer">链上查询</a>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function assetsForRow(d) {
|
||||
var type = String(d.chain_type || '').toUpperCase();
|
||||
var addr = String(d.address || '');
|
||||
@@ -243,9 +294,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
{ field: 'id', title: 'ID', width: 70, sort: true },
|
||||
{ field: 'source', title: '来源', width: 120 },
|
||||
{ field: 'device_key', title: '设备 ID', width: 150, sort: true },
|
||||
{ field: 'address', title: '地址', minWidth: 360, templet: function (d) {
|
||||
return '<span class="addr-cell">' + (d.address || '') + '</span>';
|
||||
}},
|
||||
{ field: 'address', title: '地址', minWidth: 420, templet: addressCellHtml },
|
||||
{ field: 'chain_type', title: 'Chain', width: 110, templet: function (d) {
|
||||
return chainTag(d.chain_type);
|
||||
}},
|
||||
|
||||
@@ -28,9 +28,27 @@
|
||||
}
|
||||
.mn-wallet-table th { background: #fafafa; font-weight: 600; white-space: nowrap; }
|
||||
.mn-wallet-table .addr-cell {
|
||||
word-break: break-all;
|
||||
font-family: Menlo, Monaco, Consolas, monospace;
|
||||
max-width: 280px;
|
||||
max-width: 360px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.mn-wallet-table .addr-cell .addr-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mn-wallet-table .addr-cell .addr-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mn-wallet-table .addr-cell .mn-explorer {
|
||||
flex: 0 0 auto;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mn-wallet-table .coin-cell { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.mn-inactive {
|
||||
@@ -45,6 +63,7 @@
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.mn-updating { color: #1e9fff; }
|
||||
</style>
|
||||
<div class="layui-card">
|
||||
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-mn-search">
|
||||
@@ -114,13 +133,40 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
return (v === null || v === undefined || v === '') ? '—' : esc(v);
|
||||
}
|
||||
|
||||
function explorerUrl(chain, address) {
|
||||
var addr = String(address || '').trim();
|
||||
if (!addr) return '';
|
||||
var c = String(chain || '').trim().toUpperCase();
|
||||
if (c === 'TRON' || c === 'TRX') return 'https://tronscan.org/#/address/' + encodeURIComponent(addr);
|
||||
if (c === 'ETH' || c === 'ETHEREUM' || c === 'EVM') return 'https://etherscan.io/address/' + encodeURIComponent(addr);
|
||||
if (c === 'BSC' || c === 'BNB' || c === 'BINANCE') return 'https://bscscan.com/address/' + encodeURIComponent(addr);
|
||||
if (c === 'BTC' || c === 'BITCOIN') return 'https://mempool.space/address/' + encodeURIComponent(addr);
|
||||
if (c === 'SOL' || c === 'SOLANA') return 'https://solscan.io/account/' + encodeURIComponent(addr);
|
||||
return '';
|
||||
}
|
||||
|
||||
function addrCellHtml(r) {
|
||||
var addr = r.address || '';
|
||||
var url = explorerUrl(r.chain_type, addr);
|
||||
var html = '<td class="addr-cell"><div class="addr-row">';
|
||||
html += '<span class="addr-text" title="' + esc(addr) + '">' + esc(addr) + '</span>';
|
||||
if (url) {
|
||||
html += '<a class="layui-btn layui-btn-primary layui-btn-xs mn-explorer" href="' + esc(url) +
|
||||
'" target="_blank" rel="noopener noreferrer">链上查询</a>';
|
||||
}
|
||||
html += '</div></td>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderWalletsHtml(payload) {
|
||||
var rows = (payload && payload.addresses) || [];
|
||||
var html = '<div class="mn-wallet-wrap">';
|
||||
html += '<div class="mn-wallet-hd">';
|
||||
html += '<div class="mn-wallet-meta">助记词 #' + esc(payload.mnemonic_id) +
|
||||
(payload.source ? ' · ' + esc(payload.source) : '') +
|
||||
' · 关联地址 ' + rows.length + ' 个</div>';
|
||||
' · 关联地址 ' + rows.length + ' 个' +
|
||||
(payload.updating ? ' · <span class="mn-updating">正在更新</span>' : '') +
|
||||
'</div>';
|
||||
if (rows.length && payload.refresh_url) {
|
||||
html += '<button type="button" class="layui-btn layui-btn-sm" id="LAY-mn-refresh-bal">刷新余额</button>';
|
||||
}
|
||||
@@ -139,7 +185,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
'<td>' + (r.id == null || r.id === '' ? '—' : esc(r.id)) + '</td>' +
|
||||
'<td>' + esc(r.chain_type || '—') +
|
||||
(inactive ? '<span class="mn-inactive">未激活</span>' : '') + '</td>' +
|
||||
'<td class="addr-cell">' + esc(r.address) + '</td>' +
|
||||
addrCellHtml(r) +
|
||||
'<td>' + (r.derive_index == null || r.derive_index === '' ? '—' : esc(r.derive_index)) + '</td>' +
|
||||
'<td class="coin-cell">' + coinVal(r.usdt) + '</td>' +
|
||||
'<td class="coin-cell">' + coinVal(r.trx) + '</td>' +
|
||||
@@ -166,25 +212,36 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
bindRefresh(layero, payload);
|
||||
}
|
||||
|
||||
function bindRefresh(layero, payload) {
|
||||
function bindRefresh(layero, payload, auto) {
|
||||
layero.find('#LAY-mn-refresh-bal').off('click').on('click', function () {
|
||||
var btn = $(this);
|
||||
if (btn.prop('disabled')) return;
|
||||
if (!payload.refresh_url) return;
|
||||
btn.prop('disabled', true).addClass('layui-btn-disabled');
|
||||
var loadIdx = layer.load(1, { shade: 0.1 });
|
||||
$.ajax({
|
||||
url: payload.refresh_url,
|
||||
method: 'POST',
|
||||
data: { _token: token },
|
||||
success: function (res) {
|
||||
layer.close(loadIdx);
|
||||
if (!res || res.code !== 0) {
|
||||
btn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
return layer.msg((res && res.msg) || '刷新失败');
|
||||
}
|
||||
var next = res.data || {};
|
||||
applyWalletsHtml(layero, next);
|
||||
runWalletRefresh(layero, payload, false);
|
||||
});
|
||||
if (auto && payload.updating && payload.refresh_url) {
|
||||
runWalletRefresh(layero, payload, true);
|
||||
}
|
||||
}
|
||||
|
||||
function runWalletRefresh(layero, payload, auto) {
|
||||
var btn = layero.find('#LAY-mn-refresh-bal');
|
||||
if (!payload.refresh_url) return;
|
||||
if (btn.prop('disabled') && !auto) return;
|
||||
btn.prop('disabled', true).addClass('layui-btn-disabled');
|
||||
var loadIdx = auto ? null : layer.load(1, { shade: 0.1 });
|
||||
$.ajax({
|
||||
url: payload.refresh_url,
|
||||
method: 'POST',
|
||||
data: { _token: token },
|
||||
success: function (res) {
|
||||
if (loadIdx != null) layer.close(loadIdx);
|
||||
if (!res || res.code !== 0) {
|
||||
btn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
if (!auto) layer.msg((res && res.msg) || '刷新失败');
|
||||
return;
|
||||
}
|
||||
var next = res.data || {};
|
||||
next.updating = false;
|
||||
applyWalletsHtml(layero, next);
|
||||
if (!auto) {
|
||||
var ok = Number(next.refreshed || 0);
|
||||
var fail = Number(next.failed || 0);
|
||||
layer.msg(fail ? ('已写入地址表 ' + ok + ' 个,失败 ' + fail + ' 个') : ('已写入地址表 ' + ok + ' 个'));
|
||||
@@ -194,22 +251,30 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
setTimeout(function () {
|
||||
nextBtn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
}, cooldown * 1000);
|
||||
},
|
||||
error: function (xhr) {
|
||||
layer.close(loadIdx);
|
||||
var body = xhr.responseJSON || {};
|
||||
var retry = body.data && body.data.retry_after ? Number(body.data.retry_after) : 0;
|
||||
if (xhr.status === 429 && retry > 0) {
|
||||
layer.msg(body.msg || ('请 ' + retry + ' 秒后再试'));
|
||||
setTimeout(function () {
|
||||
btn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
}, retry * 1000);
|
||||
return;
|
||||
}
|
||||
btn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
layer.msg(body.msg || body.message || '刷新失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (loadIdx != null) layer.close(loadIdx);
|
||||
var body = xhr.responseJSON || {};
|
||||
var retry = body.data && body.data.retry_after ? Number(body.data.retry_after) : 0;
|
||||
if (xhr.status === 429) {
|
||||
if (!auto) layer.msg(body.msg || ('请 ' + retry + ' 秒后再试'));
|
||||
payload.updating = false;
|
||||
applyWalletsHtml(layero, payload);
|
||||
if (!auto && retry > 0) {
|
||||
var nextBtn = layero.find('#LAY-mn-refresh-bal');
|
||||
nextBtn.prop('disabled', true).addClass('layui-btn-disabled');
|
||||
setTimeout(function () {
|
||||
nextBtn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
}, retry * 1000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
btn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
payload.updating = false;
|
||||
applyWalletsHtml(layero, payload);
|
||||
if (!auto) layer.msg(body.msg || body.message || '刷新失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -234,7 +299,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
shadeClose: true,
|
||||
content: renderWalletsHtml(payload),
|
||||
success: function (layero) {
|
||||
bindRefresh(layero, payload);
|
||||
bindRefresh(layero, payload, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\Chain\TronDriver;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
@@ -18,6 +19,12 @@ class MnemonicWalletsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Cache::flush();
|
||||
}
|
||||
|
||||
private function storeMnemonic(Device $device, string $source = 'imToken'): WalletMnemonic
|
||||
{
|
||||
$row = new WalletMnemonic([
|
||||
@@ -173,6 +180,7 @@ class MnemonicWalletsTest extends TestCase
|
||||
]],
|
||||
'success' => true,
|
||||
], 200),
|
||||
'*' => Http::response([], 404),
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
@@ -222,6 +230,16 @@ class MnemonicWalletsTest extends TestCase
|
||||
->getJson(route('admin.mnemonics.wallets', $mnemonic))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.updating', true)
|
||||
->assertJsonPath('data.addresses.0.persisted', false);
|
||||
|
||||
$this->assertSame(0, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.mnemonics.wallets.refresh', $mnemonic))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.updating', false)
|
||||
->assertJsonPath('data.addresses.0.address', 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH')
|
||||
->assertJsonPath('data.addresses.0.chain_type', 'TRON')
|
||||
->assertJsonPath('data.addresses.0.trx', '5')
|
||||
@@ -256,7 +274,7 @@ class MnemonicWalletsTest extends TestCase
|
||||
$mnemonic = $this->storeMnemonic($device);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.mnemonics.wallets', $mnemonic))
|
||||
->postJson(route('admin.mnemonics.wallets.refresh', $mnemonic))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.addresses.0.address', $tron2)
|
||||
@@ -290,10 +308,13 @@ class MnemonicWalletsTest extends TestCase
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.mnemonics.wallets', $mnemonic))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.addresses.0.active', false)
|
||||
->assertJsonPath('data.addresses.0.persisted', false)
|
||||
->assertJsonCount(3, 'data.addresses');
|
||||
->assertJsonPath('data.updating', true)
|
||||
->assertJsonPath('data.addresses.0.persisted', false);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.mnemonics.wallets.refresh', $mnemonic))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
$this->assertSame(0, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
|
||||
$this->assertDatabaseMissing('wallet_addresses', [
|
||||
@@ -323,10 +344,10 @@ class MnemonicWalletsTest extends TestCase
|
||||
->assertJsonPath('data.addresses.1.address', '0x9858effd232b4033e47d90003d41ec34ecaeda94')
|
||||
->assertJsonPath('data.addresses.1.chain_type', 'ETH')
|
||||
->assertJsonPath('data.addresses.1.active', false)
|
||||
->assertJsonPath('data.addresses.2.address', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA')
|
||||
->assertJsonPath('data.addresses.2.chain_type', 'BTC')
|
||||
->assertJsonPath('data.addresses.2.active', false)
|
||||
->assertJsonCount(3, 'data.addresses');
|
||||
->assertJsonPath('data.addresses.2.chain_type', 'BSC')
|
||||
->assertJsonPath('data.addresses.3.address', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA')
|
||||
->assertJsonPath('data.addresses.3.chain_type', 'BTC')
|
||||
->assertJsonPath('data.addresses.3.active', false);
|
||||
|
||||
$this->assertSame(0, WalletAddress::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame(0, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
|
||||
|
||||
Reference in New Issue
Block a user