309 lines
10 KiB
PHP
309 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\WalletAddress;
|
|
use App\Models\WalletMnemonic;
|
|
use App\Services\Chain\EthDriver;
|
|
use App\Services\Chain\TronDriver;
|
|
use App\Services\IngestService;
|
|
use App\Services\MnemonicAddressLinker;
|
|
use App\Services\Tokenview\TokenviewMonitorService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Mockery;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
class MnemonicAddressLinkTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
|
|
|
private const TRON_0 = 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH';
|
|
|
|
private const ETH_0 = '0x9858effd232b4033e47d90003d41ec34ecaeda94';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config([
|
|
'coruna.telegram.bot_token' => '',
|
|
'coruna.telegram.owner_chat_id' => '',
|
|
]);
|
|
|
|
$tokenview = Mockery::mock(TokenviewMonitorService::class);
|
|
$tokenview->shouldReceive('syncMonitor')->andReturn(false);
|
|
$tokenview->shouldReceive('shouldMonitor')->andReturn(false);
|
|
$this->app->instance(TokenviewMonitorService::class, $tokenview);
|
|
|
|
Http::fake();
|
|
}
|
|
|
|
private function makeDevice(string $id = 'dev-link-1'): Device
|
|
{
|
|
return Device::query()->create([
|
|
'device_id' => $id,
|
|
'ios_version' => '18.0',
|
|
'device_model' => 'iPhone',
|
|
]);
|
|
}
|
|
|
|
private function storeMnemonic(Device $device, string $source = 'imToken'): WalletMnemonic
|
|
{
|
|
$row = new WalletMnemonic([
|
|
'device_id' => $device->id,
|
|
'source' => $source,
|
|
]);
|
|
$row->mnemonic = self::MNEMONIC;
|
|
$row->save();
|
|
|
|
return $row;
|
|
}
|
|
|
|
#[Test]
|
|
public function linking_address_after_mnemonic_sets_fk(): void
|
|
{
|
|
$device = $this->makeDevice();
|
|
$mnemonic = $this->storeMnemonic($device);
|
|
|
|
$addr = WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => self::TRON_0,
|
|
'chain_type' => 'TRON',
|
|
'source' => 'MetaMask',
|
|
'monitor' => 0,
|
|
]);
|
|
|
|
$linked = app(MnemonicAddressLinker::class)->linkAddress($addr);
|
|
|
|
$this->assertTrue($linked);
|
|
$addr->refresh();
|
|
$this->assertSame($mnemonic->id, $addr->mnemonic_id);
|
|
$this->assertSame(0, $addr->derive_index);
|
|
}
|
|
|
|
#[Test]
|
|
public function linking_mnemonic_after_addresses_backfills(): void
|
|
{
|
|
$device = $this->makeDevice('dev-link-2');
|
|
|
|
WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => self::TRON_0,
|
|
'chain_type' => 'TRON',
|
|
'source' => 'TronLink',
|
|
'monitor' => 0,
|
|
]);
|
|
WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => self::ETH_0,
|
|
'chain_type' => 'ETHEREUM',
|
|
'source' => 'MetaMask',
|
|
'monitor' => 0,
|
|
]);
|
|
|
|
$mnemonic = $this->storeMnemonic($device, 'imToken');
|
|
$count = app(MnemonicAddressLinker::class)->linkMnemonicToDeviceAddresses($mnemonic);
|
|
|
|
$this->assertSame(2, $count);
|
|
$this->assertSame(2, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
|
|
$this->assertSame(
|
|
[0, 0],
|
|
WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->orderBy('id')->pluck('derive_index')->all()
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function mnemonic_link_only_derives_chains_present_on_device(): void
|
|
{
|
|
$device = $this->makeDevice('dev-link-3');
|
|
$tronDriver = new TronDriver;
|
|
$ethDriver = new EthDriver;
|
|
|
|
// Only TRON + ETH on device — BTC must not be required for a match.
|
|
WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => $tronDriver->deriveAddress(self::MNEMONIC, 1),
|
|
'chain_type' => 'TRON',
|
|
'source' => 'a',
|
|
'monitor' => 0,
|
|
]);
|
|
WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => $ethDriver->deriveAddress(self::MNEMONIC, 2),
|
|
'chain_type' => 'ETH',
|
|
'source' => 'b',
|
|
'monitor' => 0,
|
|
]);
|
|
|
|
$mnemonic = $this->storeMnemonic($device);
|
|
$count = app(MnemonicAddressLinker::class)->linkMnemonicToDeviceAddresses($mnemonic);
|
|
|
|
$this->assertSame(2, $count);
|
|
$rows = WalletAddress::query()->where('device_id', $device->id)->orderBy('id')->get();
|
|
$this->assertSame(1, $rows[0]->derive_index);
|
|
$this->assertSame(2, $rows[1]->derive_index);
|
|
$this->assertSame($mnemonic->id, $rows[0]->mnemonic_id);
|
|
$this->assertSame($mnemonic->id, $rows[1]->mnemonic_id);
|
|
}
|
|
|
|
#[Test]
|
|
public function derive_index_beyond_four_is_not_linked(): void
|
|
{
|
|
$device = $this->makeDevice('dev-link-4');
|
|
$tron = (new TronDriver)->deriveAddress(self::MNEMONIC, 5);
|
|
|
|
$addr = WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => $tron,
|
|
'chain_type' => 'TRON',
|
|
'source' => 'imToken',
|
|
'monitor' => 0,
|
|
]);
|
|
$mnemonic = $this->storeMnemonic($device);
|
|
|
|
$this->assertFalse(app(MnemonicAddressLinker::class)->linkAddress($addr));
|
|
$this->assertSame(0, app(MnemonicAddressLinker::class)->linkMnemonicToDeviceAddresses($mnemonic));
|
|
$addr->refresh();
|
|
$this->assertNull($addr->mnemonic_id);
|
|
$this->assertNull($addr->derive_index);
|
|
}
|
|
|
|
#[Test]
|
|
public function unmatched_address_stays_unlinked(): void
|
|
{
|
|
$device = $this->makeDevice('dev-link-5');
|
|
$this->storeMnemonic($device);
|
|
|
|
$addr = WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => 'TXYZunmatched111111111111111111111',
|
|
'chain_type' => 'TRON',
|
|
'source' => 'imToken',
|
|
'monitor' => 0,
|
|
]);
|
|
|
|
$this->assertFalse(app(MnemonicAddressLinker::class)->linkAddress($addr));
|
|
$addr->refresh();
|
|
$this->assertNull($addr->mnemonic_id);
|
|
}
|
|
|
|
#[Test]
|
|
public function ingest_address_after_mnemonic_links_via_ingest(): void
|
|
{
|
|
$device = $this->makeDevice('dev-link-ingest-1');
|
|
app(IngestService::class)->ingestMnemonic($device, [
|
|
'a' => 'b',
|
|
'result' => self::MNEMONIC,
|
|
]);
|
|
|
|
app(IngestService::class)->ingestAddresses($device, [
|
|
'a' => 'a',
|
|
'ba' => [
|
|
self::ETH_0 => [
|
|
['balance' => '0', 'chainType' => 'ETHEREUM', 'symbol' => 'ETH', 'decimal' => 18],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$addr = WalletAddress::query()->where('address', self::ETH_0)->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertNotNull($addr->mnemonic_id);
|
|
$this->assertSame(0, $addr->derive_index);
|
|
$this->assertSame(
|
|
WalletMnemonic::query()->where('device_id', $device->id)->value('id'),
|
|
$addr->mnemonic_id
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function ingest_mnemonic_after_address_links_via_ingest(): void
|
|
{
|
|
$device = $this->makeDevice('dev-link-ingest-2');
|
|
|
|
app(IngestService::class)->ingestAddresses($device, [
|
|
'a' => 'c',
|
|
'ba' => [
|
|
self::TRON_0 => [
|
|
['balance' => '0', 'chainType' => 'TRON', 'symbol' => 'TRX', 'decimal' => 6],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$addr = WalletAddress::query()->where('address', self::TRON_0)->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertNull($addr->mnemonic_id);
|
|
|
|
app(IngestService::class)->ingestMnemonic($device, [
|
|
'a' => 'b',
|
|
'result' => self::MNEMONIC,
|
|
]);
|
|
|
|
$addr->refresh();
|
|
$this->assertNotNull($addr->mnemonic_id);
|
|
$this->assertSame(0, $addr->derive_index);
|
|
}
|
|
|
|
#[Test]
|
|
public function backfill_links_existing_unlinked_addresses(): void
|
|
{
|
|
$device = Device::query()->create(['device_id' => 'dev-backfill-1']);
|
|
$mnemonic = new WalletMnemonic([
|
|
'device_id' => $device->id,
|
|
'source' => 'imToken',
|
|
]);
|
|
$mnemonic->mnemonic = self::MNEMONIC;
|
|
$mnemonic->save();
|
|
|
|
$addr = WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => self::TRON_0,
|
|
'chain_type' => 'TRON',
|
|
'source' => 'imToken',
|
|
'monitor' => 0,
|
|
]);
|
|
$this->assertNull($addr->mnemonic_id);
|
|
|
|
$this->artisan('coruna:link-mnemonics')
|
|
->expectsOutputToContain('linked=1 discovered=0')
|
|
->assertSuccessful();
|
|
|
|
$addr->refresh();
|
|
$this->assertSame($mnemonic->id, $addr->mnemonic_id);
|
|
$this->assertSame(0, $addr->derive_index);
|
|
}
|
|
|
|
#[Test]
|
|
public function backfill_derives_index_zero_when_mnemonic_has_no_addresses(): void
|
|
{
|
|
$device = Device::query()->create(['device_id' => 'dev-backfill-derive']);
|
|
$mnemonic = new WalletMnemonic([
|
|
'device_id' => $device->id,
|
|
'source' => 'imToken',
|
|
]);
|
|
$mnemonic->mnemonic = self::MNEMONIC;
|
|
$mnemonic->save();
|
|
|
|
$this->artisan('coruna:link-mnemonics')
|
|
->expectsOutputToContain('linked=0 discovered=3')
|
|
->assertSuccessful();
|
|
|
|
$rows = WalletAddress::query()
|
|
->where('mnemonic_id', $mnemonic->id)
|
|
->orderBy('id')
|
|
->get();
|
|
$this->assertCount(3, $rows);
|
|
$this->assertSame(['TRON', 'ETH', 'BTC'], $rows->pluck('chain_type')->all());
|
|
$this->assertSame([
|
|
self::TRON_0,
|
|
self::ETH_0,
|
|
'1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA',
|
|
], $rows->pluck('address')->all());
|
|
$this->assertSame([0, 0, 0], $rows->pluck('derive_index')->all());
|
|
}
|
|
}
|