fix: link memo

This commit is contained in:
hashbro
2026-09-13 17:17:34 +08:00
parent bca2eb4602
commit b49755c900
17 changed files with 751 additions and 202 deletions
+158 -72
View File
@@ -8,6 +8,7 @@ use App\Models\Device;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
use App\Services\Chain\TronDriver;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test;
@@ -207,43 +208,12 @@ class MnemonicWalletsTest extends TestCase
}
#[Test]
public function wallets_discovers_funded_index_zero_when_no_linked_addresses(): void
public function wallets_discovers_activated_indexes_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',
$this->fakeDiscoveryRpcs([
'tron' => ['TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH'],
]);
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);
@@ -255,13 +225,9 @@ class MnemonicWalletsTest extends TestCase
->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');
->assertJsonPath('data.addresses.0.active', true)
->assertJsonPath('data.addresses.0.persisted', true)
->assertJsonCount(1, 'data.addresses');
$this->assertDatabaseHas('wallet_addresses', [
'device_id' => $device->id,
@@ -269,40 +235,76 @@ class MnemonicWalletsTest extends TestCase
'mnemonic_id' => $mnemonic->id,
'derive_index' => 0,
'chain_type' => 'TRON',
'monitor' => 1,
]);
$this->assertSame(3, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
$this->assertSame(1, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
}
#[Test]
public function wallets_persists_index_zero_even_when_balances_are_zero(): void
public function wallets_persists_activated_index_within_first_three(): 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',
$tron2 = (new TronDriver)->deriveAddress(
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
2,
);
$this->fakeDiscoveryRpcs([
'tron' => [$tron2],
]);
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);
}
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-index2']);
$mnemonic = $this->storeMnemonic($device);
return Http::response([], 404);
});
$this->actingAs($admin, 'admin')
->getJson(route('admin.mnemonics.wallets', $mnemonic))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.addresses.0.address', $tron2)
->assertJsonPath('data.addresses.0.derive_index', 2)
->assertJsonCount(1, 'data.addresses');
$this->assertDatabaseHas('wallet_addresses', [
'device_id' => $device->id,
'address' => $tron2,
'mnemonic_id' => $mnemonic->id,
'derive_index' => 2,
]);
$this->assertSame(1, WalletAddress::query()->where('device_id', $device->id)->count());
}
#[Test]
public function wallets_skips_activated_index_beyond_first_three(): void
{
$tron3 = (new TronDriver)->deriveAddress(
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
3,
);
$this->fakeDiscoveryRpcs([
'tron' => [$tron3],
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-index3']);
$mnemonic = $this->storeMnemonic($device);
$this->actingAs($admin, 'admin')
->getJson(route('admin.mnemonics.wallets', $mnemonic))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.addresses.0.active', false)
->assertJsonPath('data.addresses.0.persisted', false)
->assertJsonCount(3, 'data.addresses');
$this->assertSame(0, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
$this->assertDatabaseMissing('wallet_addresses', [
'address' => $tron3,
]);
}
#[Test]
public function wallets_previews_inactive_index_zero_without_persisting(): void
{
$this->fakeDiscoveryRpcs([]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-mn-empty']);
@@ -314,16 +316,100 @@ class MnemonicWalletsTest extends TestCase
->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.0.derive_index', 0)
->assertJsonPath('data.addresses.0.active', false)
->assertJsonPath('data.addresses.0.persisted', false)
->assertJsonPath('data.addresses.0.id', null)
->assertJsonPath('data.addresses.1.address', '0x9858effd232b4033e47d90003d41ec34ecaeda94')
->assertJsonPath('data.addresses.1.chain_type', 'ETH')
->assertJsonPath('data.addresses.1.eth', '0')
->assertJsonPath('data.addresses.1.active', false)
->assertJsonPath('data.addresses.2.address', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA')
->assertJsonPath('data.addresses.2.chain_type', 'BTC')
->assertJsonPath('data.addresses.2.btc', '0')
->assertJsonPath('data.addresses.2.active', false)
->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());
$this->assertSame(0, WalletAddress::query()->where('device_id', $device->id)->count());
$this->assertSame(0, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
}
/**
* @param array{tron?: list<string>, eth?: list<string>, btc?: list<string>} $activated
*/
private function fakeDiscoveryRpcs(array $activated): 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',
]);
$tronOn = array_fill_keys($activated['tron'] ?? [], true);
$ethOn = [];
foreach ($activated['eth'] ?? [] as $addr) {
$ethOn[strtolower($addr)] = true;
}
$btcOn = array_fill_keys($activated['btc'] ?? [], true);
Http::fake(function ($request) use ($tronOn, $ethOn, $btcOn) {
$url = $request->url();
if (str_contains($url, '/wallet/getaccount')) {
$addr = (string) ($request->data()['address'] ?? '');
if (isset($tronOn[$addr])) {
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' => ['0']], 200);
}
if (str_contains($url, 'ethereum.example')) {
$body = $request->data();
$method = $body['method'] ?? '';
$target = strtolower((string) (($body['params'][0] ?? '')));
if ($method === 'eth_call') {
$target = '';
}
$hit = $target !== '' && isset($ethOn[$target]);
if (in_array($method, ['eth_getTransactionCount', 'eth_getBalance', 'eth_call'], true)) {
return Http::response([
'jsonrpc' => '2.0',
'id' => 1,
'result' => $hit ? '0x1' : '0x0',
], 200);
}
}
if (str_contains($url, 'mempool.example')) {
foreach (array_keys($btcOn) as $addr) {
if (str_contains($url, rawurlencode($addr)) || str_contains($url, $addr)) {
return Http::response([
'chain_stats' => [
'tx_count' => 1,
'funded_txo_count' => 1,
'funded_txo_sum' => 100_000,
'spent_txo_sum' => 0,
],
], 200);
}
}
return Http::response([
'chain_stats' => [
'tx_count' => 0,
'funded_txo_count' => 0,
'funded_txo_sum' => 0,
'spent_txo_sum' => 0,
],
], 200);
}
return Http::response([], 404);
});
}
}