feat: alchemy
This commit is contained in:
@@ -142,8 +142,10 @@ class MnemonicWalletDiscovery
|
||||
return count($saved);
|
||||
});
|
||||
|
||||
// Scheduled run: mark complete once every chain has been probed at least once.
|
||||
if (! $force && count($covered) === count(self::CHAINS)) {
|
||||
// Scheduled run: mark complete as soon as derivation has run at least once
|
||||
// (any chain covered — linked / miss-cached / probed). Subsequent scheduled
|
||||
// runs skip this mnemonic entirely; manual force=true still re-probes.
|
||||
if (! $force && $covered !== []) {
|
||||
WalletMnemonic::query()
|
||||
->whereKey($mnemonic->id)
|
||||
->update([
|
||||
@@ -260,7 +262,7 @@ class MnemonicWalletDiscovery
|
||||
continue;
|
||||
}
|
||||
|
||||
$saved[] = $this->persistActivated(
|
||||
$persisted = $this->persistActivated(
|
||||
$mnemonic,
|
||||
$source,
|
||||
$chainType,
|
||||
@@ -268,6 +270,9 @@ class MnemonicWalletDiscovery
|
||||
$index,
|
||||
$probe['coins'],
|
||||
);
|
||||
if ($persisted !== null) {
|
||||
$saved[] = $persisted;
|
||||
}
|
||||
}
|
||||
|
||||
return $saved;
|
||||
@@ -483,7 +488,13 @@ class MnemonicWalletDiscovery
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist (or refresh) an activated derived address. Returns the row only
|
||||
* when this is a first-time association (new row, or previously unlinked) so
|
||||
* the caller can fire a single "可归集" notification. Already-linked rows are
|
||||
* refreshed silently and return null to avoid duplicate notifications.
|
||||
*
|
||||
* @param array<string, string> $coins
|
||||
* @return WalletAddress|null null when the row was already linked to this mnemonic
|
||||
*/
|
||||
private function persistActivated(
|
||||
WalletMnemonic $mnemonic,
|
||||
@@ -492,7 +503,7 @@ class MnemonicWalletDiscovery
|
||||
string $address,
|
||||
int $index,
|
||||
array $coins,
|
||||
): WalletAddress {
|
||||
): ?WalletAddress {
|
||||
$row = WalletAddress::query()->firstOrNew([
|
||||
'device_id' => $mnemonic->device_id,
|
||||
'address' => $address,
|
||||
@@ -500,11 +511,16 @@ class MnemonicWalletDiscovery
|
||||
'chain_type' => $chainType,
|
||||
]);
|
||||
|
||||
// First-time association = brand-new row OR an existing row that had no
|
||||
// mnemonic link yet. Only these warrant a collectable notification.
|
||||
$isNew = ! $row->exists;
|
||||
$wasUnlinked = $isNew || $row->getOriginal('mnemonic_id') === null;
|
||||
|
||||
$row->chain_type = $chainType;
|
||||
$row->mnemonic_id = $mnemonic->id;
|
||||
$row->derive_index = $index;
|
||||
$row->monitor = 1;
|
||||
if (! $row->exists) {
|
||||
if ($isNew) {
|
||||
$row->monitor_synced = false;
|
||||
$row->monitor_failures = 0;
|
||||
}
|
||||
@@ -514,7 +530,7 @@ class MnemonicWalletDiscovery
|
||||
$row->save();
|
||||
$this->enableMonitor($row);
|
||||
|
||||
return $row;
|
||||
return $wasUnlinked ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -201,4 +201,77 @@ class CollectableAddressNotifyTest extends TestCase
|
||||
&& ($request->data()['chat_id'] ?? null) === '10001';
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rediscovery_does_not_resend_collectable_telegram(): void
|
||||
{
|
||||
Http::fake(function ($request) {
|
||||
$url = $request->url();
|
||||
if (str_contains($url, 'api.telegram.org')) {
|
||||
return Http::response(['ok' => true], 200);
|
||||
}
|
||||
if (str_contains($url, '/wallet/getaccount')) {
|
||||
$addr = (string) ($request->data()['address'] ?? '');
|
||||
if ($addr === self::TRON_0) {
|
||||
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' => [dechex(2_000_000)]], 200);
|
||||
}
|
||||
if (str_contains($url, 'ethereum.example')) {
|
||||
$data = $request->data();
|
||||
$method = $data['method'] ?? '';
|
||||
$target = strtolower((string) ($data['params'][0] ?? ''));
|
||||
$hit = $target === self::ETH_0;
|
||||
if ($method === 'eth_getTransactionCount') {
|
||||
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => $hit ? '0x1' : '0x0'], 200);
|
||||
}
|
||||
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')) {
|
||||
$activated = str_contains($url, self::BTC_0);
|
||||
|
||||
return Http::response([
|
||||
'chain_stats' => [
|
||||
'tx_count' => $activated ? 1 : 0,
|
||||
'funded_txo_count' => $activated ? 1 : 0,
|
||||
'funded_txo_sum' => $activated ? 100_000 : 0,
|
||||
'spent_txo_sum' => 0,
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
return Http::response([], 404);
|
||||
});
|
||||
|
||||
$device = Device::query()->create(['device_id' => 'dev-collect-rediscover']);
|
||||
$mnemonic = $this->storeMnemonic($device);
|
||||
|
||||
$discovery = app(MnemonicWalletDiscovery::class);
|
||||
|
||||
// First pass: 3 activated addresses discovered + notified.
|
||||
$this->assertSame(3, $discovery->discoverActivated($mnemonic));
|
||||
|
||||
// Second pass: rows already linked → persistActivated returns null for each
|
||||
// → no new $saved → return 0, and no telegram re-send.
|
||||
$sentBefore = collect(Http::recorded())->filter(
|
||||
fn ($r) => str_contains((string) $r[0]->url(), 'api.telegram.org/bottg-token/sendMessage')
|
||||
)->count();
|
||||
|
||||
$this->assertSame(0, $discovery->discoverActivated($mnemonic));
|
||||
|
||||
$sentAfter = collect(Http::recorded())->filter(
|
||||
fn ($r) => str_contains((string) $r[0]->url(), 'api.telegram.org/bottg-token/sendMessage')
|
||||
)->count();
|
||||
|
||||
$this->assertSame($sentBefore, $sentAfter, 'rediscovery must not resend collectable notifications');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user