feat: alchemy
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user