feat: alchemy

This commit is contained in:
hashbro
2026-09-29 05:25:55 +08:00
parent 2c87d37051
commit 15c45a4fd2
14 changed files with 871 additions and 24 deletions
+151
View File
@@ -3,6 +3,7 @@
namespace App\Services;
use App\Models\WalletAddress;
use App\Services\Alchemy\AlchemyBalanceService;
use App\Services\Chain\ChainManager;
use App\Services\Chain\TronDriver;
use Illuminate\Support\Facades\Http;
@@ -19,6 +20,7 @@ class WalletBalanceService
{
public function __construct(
private readonly ChainManager $chains,
private readonly AlchemyBalanceService $alchemy,
) {}
/**
@@ -47,6 +49,14 @@ class WalletBalanceService
return false;
}
if ($this->alchemy->isEnabled()) {
$ok = $this->refreshEvmViaAlchemy($address, $addr, 'ETH', 'eth');
if ($ok) {
return true;
}
// Alchemy failed → fall through to RPC.
}
try {
$driver = $this->chains->resolve('eth');
$attrs = [
@@ -69,6 +79,7 @@ class WalletBalanceService
'eth' => $address->eth,
'usdt' => $address->usdt,
'bnb' => $address->bnb,
'source' => 'rpc',
]);
return true;
@@ -82,6 +93,93 @@ class WalletBalanceService
}
}
/**
* Alchemy path for ETH/BSC refresh. One eth_getBalance + one eth_call (USDT).
* For ETH, also pull the BSC sibling (BNB + BEP20 USDT) via Alchemy when the
* BSC network is enabled, else via the BSC RPC driver.
*/
private function refreshEvmViaAlchemy(WalletAddress $address, string $addr, string $nativeSymbol, string $network): bool
{
$result = $this->alchemy->fetch($nativeSymbol, $addr);
if ($result === null) {
return false;
}
$attrs = [];
if ($result['native'] !== null) {
$attrs[strtolower($nativeSymbol)] = $result['native']['amount'];
}
$usdt = $this->findTokenBySymbol($result['tokens'], 'USDT');
if ($usdt !== null) {
$attrs['usdt'] = $usdt['amount'];
}
// ETH address == BSC address: also pull BNB + BEP20 USDT and persist sibling.
if ($nativeSymbol === 'ETH') {
$bsc = $this->alchemy->fetch('BSC', $addr);
if ($bsc !== null) {
if ($bsc['native'] !== null) {
$attrs['bnb'] = $bsc['native']['amount'];
}
$bscUsdt = $this->findTokenBySymbol($bsc['tokens'], 'USDT');
$this->persistBscSibling($address, [
'bnb' => $bsc['native']['amount'] ?? '0',
'usdt' => $bscUsdt !== null ? $bscUsdt['amount'] : '0',
]);
} else {
// Alchemy BSC not enabled → fall back to BSC RPC for the sibling.
$bscCoins = $this->bscCoins($addr);
if ($bscCoins !== null) {
$attrs['bnb'] = $bscCoins['bnb'];
$this->persistBscSibling($address, $bscCoins);
}
}
}
$this->persistCoins($address, $attrs);
Log::info('eth balance refresh ok', [
'wallet_address_id' => $address->id,
'address' => $addr,
'source' => 'alchemy',
]);
return true;
}
/**
* @param list<array<string, mixed>> $tokens
*/
private function findTokenBySymbol(array $tokens, string $symbol): ?array
{
$symbol = strtoupper(trim($symbol));
foreach ($tokens as $t) {
if (strtoupper((string) ($t['symbol'] ?? '')) === $symbol) {
return $t;
}
}
return null;
}
/**
* @param list<array<string, mixed>> $tokens
*/
private function findTokenByContract(array $tokens, string $contract): ?array
{
$contract = strtolower($contract);
if ($contract === '') {
return null;
}
foreach ($tokens as $t) {
if (strtolower((string) ($t['contract'] ?? '')) === $contract) {
return $t;
}
}
return null;
}
public function refreshBsc(WalletAddress $address): bool
{
$addr = trim((string) $address->address);
@@ -89,6 +187,30 @@ class WalletBalanceService
return false;
}
if ($this->alchemy->isEnabled()) {
$result = $this->alchemy->fetch('BSC', $addr);
if ($result !== null) {
$attrs = [];
if ($result['native'] !== null) {
$attrs['bnb'] = $result['native']['amount'];
}
$usdt = $this->findTokenBySymbol($result['tokens'], 'USDT');
if ($usdt !== null) {
$attrs['usdt'] = $usdt['amount'];
}
$this->persistCoins($address, $attrs);
Log::info('bsc balance refresh ok', [
'wallet_address_id' => $address->id,
'address' => $addr,
'source' => 'alchemy',
]);
return true;
}
// Alchemy failed → fall through to RPC.
}
try {
$coins = $this->bscCoins($addr);
if ($coins === null) {
@@ -104,6 +226,7 @@ class WalletBalanceService
'address' => $addr,
'bnb' => $address->bnb,
'usdt' => $address->usdt,
'source' => 'rpc',
]);
return true;
@@ -227,6 +350,7 @@ class WalletBalanceService
return false;
}
// Alchemy has no BTC chain RPC; BTC stays on the BtcDriver (mempool.space).
try {
$driver = $this->chains->resolve('btc');
$this->persistCoins($address, [
@@ -237,6 +361,7 @@ class WalletBalanceService
'wallet_address_id' => $address->id,
'address' => $addr,
'btc' => $address->btc,
'source' => 'rpc',
]);
return true;
@@ -257,6 +382,30 @@ class WalletBalanceService
return false;
}
if ($this->alchemy->isEnabled()) {
$result = $this->alchemy->fetch('SOL', $addr);
if ($result !== null) {
$attrs = [];
if ($result['native'] !== null) {
$attrs['sol'] = $result['native']['amount'];
}
$usdt = $this->findTokenBySymbol($result['tokens'], 'USDT');
if ($usdt !== null) {
$attrs['usdt'] = $usdt['amount'];
}
$this->persistCoins($address, $attrs);
Log::info('sol balance refresh ok', [
'wallet_address_id' => $address->id,
'address' => $addr,
'source' => 'alchemy',
]);
return true;
}
// Alchemy failed → fall through to SolDriver RPC.
}
try {
$driver = $this->chains->resolve('sol');
$attrs = [
@@ -273,6 +422,7 @@ class WalletBalanceService
'address' => $addr,
'sol' => $address->sol,
'usdt' => $address->usdt,
'source' => 'rpc',
]);
return true;
@@ -444,4 +594,5 @@ class WalletBalanceService
return $human === '' ? '0' : $human;
}
}