feat: bnb

This commit is contained in:
hashbro
2026-09-25 13:10:34 +08:00
parent 13aa587099
commit 534b36a2d2
24 changed files with 242 additions and 33 deletions
@@ -61,6 +61,7 @@ class AgentUserController extends Controller
'auto_transfer_threshold_trx' => $u->auto_transfer_threshold_trx !== null ? (string) $u->auto_transfer_threshold_trx : '',
'auto_transfer_threshold_eth' => $u->auto_transfer_threshold_eth !== null ? (string) $u->auto_transfer_threshold_eth : '',
'auto_transfer_threshold_btc' => $u->auto_transfer_threshold_btc !== null ? (string) $u->auto_transfer_threshold_btc : '',
'auto_transfer_threshold_bnb' => $u->auto_transfer_threshold_bnb !== null ? (string) $u->auto_transfer_threshold_bnb : '',
'album_storage_default' => $u->albumStorageDefaultEnabled() ? 1 : 0,
'created_at' => optional($u->created_at)->format('Y-m-d H:i:s'),
'updated_at' => optional($u->updated_at)->format('Y-m-d H:i:s'),
@@ -93,6 +94,7 @@ class AgentUserController extends Controller
'auto_transfer_threshold_trx' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_btc' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_bnb' => ['nullable', 'numeric', 'min:0'],
'album_storage_default' => ['nullable', 'integer', Rule::in([0, 1])],
'can_reveal_mnemonics' => ['nullable', 'integer', Rule::in([0, 1])],
]);
@@ -108,6 +110,7 @@ class AgentUserController extends Controller
'auto_transfer_threshold_trx' => $this->nullableThreshold($data['auto_transfer_threshold_trx'] ?? null),
'auto_transfer_threshold_eth' => $this->nullableThreshold($data['auto_transfer_threshold_eth'] ?? null),
'auto_transfer_threshold_btc' => $this->nullableThreshold($data['auto_transfer_threshold_btc'] ?? null),
'auto_transfer_threshold_bnb' => $this->nullableThreshold($data['auto_transfer_threshold_bnb'] ?? null),
'album_storage_default' => (bool) ((int) ($data['album_storage_default'] ?? 0)),
'can_reveal_mnemonics' => (bool) ((int) ($data['can_reveal_mnemonics'] ?? 0)),
]);
@@ -127,6 +130,7 @@ class AgentUserController extends Controller
'auto_transfer_threshold_trx' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_btc' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_bnb' => ['nullable', 'numeric', 'min:0'],
'album_storage_default' => ['nullable', 'integer', Rule::in([0, 1])],
'can_reveal_mnemonics' => ['nullable', 'integer', Rule::in([0, 1])],
]);
@@ -149,7 +153,7 @@ class AgentUserController extends Controller
if (array_key_exists('can_reveal_mnemonics', $data) && $data['can_reveal_mnemonics'] !== null) {
$agent->can_reveal_mnemonics = (bool) ((int) $data['can_reveal_mnemonics']);
}
foreach (['usdt', 'trx', 'eth', 'btc'] as $coin) {
foreach (['usdt', 'trx', 'eth', 'btc', 'bnb'] as $coin) {
$key = 'auto_transfer_threshold_'.$coin;
if (array_key_exists($key, $data)) {
$agent->{$key} = $this->nullableThreshold($data[$key] ?? null);
@@ -46,6 +46,7 @@ class SystemSettingsController extends Controller
'auto_transfer_threshold_trx' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_btc' => ['nullable', 'numeric', 'min:0'],
'auto_transfer_threshold_bnb' => ['nullable', 'numeric', 'min:0'],
'transfer_fee_address_tron' => ['nullable', 'string', 'max:128'],
'transfer_fee_private_key_tron' => ['nullable', 'string', 'max:255'],
'transfer_fee_topup_trx' => ['nullable', 'numeric', 'min:0.000001'],
@@ -81,6 +82,7 @@ class SystemSettingsController extends Controller
'auto_transfer.threshold_trx' => $threshold($data['auto_transfer_threshold_trx'] ?? null) ?? '',
'auto_transfer.threshold_eth' => $threshold($data['auto_transfer_threshold_eth'] ?? null) ?? '',
'auto_transfer.threshold_btc' => $threshold($data['auto_transfer_threshold_btc'] ?? null) ?? '',
'auto_transfer.threshold_bnb' => $threshold($data['auto_transfer_threshold_bnb'] ?? null) ?? '',
'transfer.fee_address_tron' => $threshold($data['transfer_fee_address_tron'] ?? null) ?? '',
'transfer.fee_private_key_tron' => $threshold($data['transfer_fee_private_key_tron'] ?? null),
'transfer.fee_topup_trx' => $threshold($data['transfer_fee_topup_trx'] ?? null) ?? '20',
@@ -23,7 +23,7 @@ class TransferRecordController extends Controller
'eth' => 'ETH',
'btc' => 'BTC',
],
'assets' => ['USDT', 'TRX', 'ETH', 'BTC'],
'assets' => ['USDT', 'TRX', 'ETH', 'BTC', 'BNB'],
]);
}
@@ -160,7 +160,7 @@ class WalletAddressController extends Controller
$amount = null;
} else {
$decimals = match ($asset) {
'ETH' => 18,
'ETH', 'BNB' => 18,
'BTC' => 8,
default => 6,
};
@@ -233,7 +233,7 @@ class WalletAddressController extends Controller
private function assetsForChain(string $chain): array
{
return match ($chain) {
'eth' => ['ETH', 'USDT'],
'eth' => ['ETH', 'USDT', 'BNB'],
'btc' => ['BTC'],
default => ['USDT', 'TRX'],
};
+2
View File
@@ -14,6 +14,7 @@ class User extends Authenticatable
'auto_transfer_threshold_trx',
'auto_transfer_threshold_eth',
'auto_transfer_threshold_btc',
'auto_transfer_threshold_bnb',
'album_storage_default',
'can_reveal_mnemonics',
];
@@ -41,6 +42,7 @@ class User extends Authenticatable
'auto_transfer_threshold_trx' => 'decimal:8',
'auto_transfer_threshold_eth' => 'decimal:8',
'auto_transfer_threshold_btc' => 'decimal:8',
'auto_transfer_threshold_bnb' => 'decimal:8',
'album_storage_default' => 'boolean',
'can_reveal_mnemonics' => 'boolean',
'google_auth_open' => 'integer',
+1 -1
View File
@@ -16,7 +16,7 @@ use App\Models\WalletAddress;
class AutoTransferConfig
{
/** @var list<string> */
public const ASSETS = ['USDT', 'TRX', 'ETH', 'BTC'];
public const ASSETS = ['USDT', 'TRX', 'ETH', 'BTC', 'BNB'];
public function isEnabledForAddress(WalletAddress $address): bool
{
+3 -2
View File
@@ -134,6 +134,7 @@ class AutoTransferService
'trx' => $address->trx,
'eth' => $address->eth,
'btc' => $address->btc,
'bnb' => $address->bnb,
],
];
@@ -365,7 +366,7 @@ class AutoTransferService
private function assetScale(string $asset): int
{
return match (strtoupper($asset)) {
'ETH' => 18,
'ETH', 'BNB' => 18,
'BTC' => 8,
default => 6,
};
@@ -378,7 +379,7 @@ class AutoTransferService
{
return match ($chain) {
'TRON', 'TRX' => ['USDT', 'TRX'],
'ETH', 'ETHEREUM', 'EVM' => ['ETH', 'USDT'],
'ETH', 'ETHEREUM', 'EVM' => ['ETH', 'USDT', 'BNB'],
'BTC', 'BITCOIN' => ['BTC'],
default => [],
};
+36 -15
View File
@@ -41,7 +41,7 @@ class EthDriver implements ChainDriver
$derived = Bip44::derive($mnemonic, $this->path($index));
$from = EthAddress::fromUncompressedPublicKey($derived['public_key_uncompressed']);
$units = $this->toTokenUnits($amount);
$units = $this->toTokenUnits($amount, $this->tokenDecimals($contract));
// transfer(address,uint256) selector = a9059cbb
$data = '0xa9059cbb'.EthAddress::toWord($to).str_pad(gmp_strval(gmp_init($units, 10), 16), 64, '0', STR_PAD_LEFT);
@@ -76,16 +76,22 @@ class EthDriver implements ChainDriver
// fall through to token
}
$contract = trim((string) config('coruna.eth.usdt_contract', ''));
if ($contract === '') {
return false;
foreach (['coruna.eth.usdt_contract', 'coruna.eth.bnb_contract'] as $key) {
$contract = trim((string) config($key, ''));
if ($contract === '') {
continue;
}
try {
if (bccomp($this->getTokenBalance($address, $contract), '0', 18) > 0) {
return true;
}
} catch (\Throwable) {
// try the next token
}
}
try {
return bccomp($this->getTokenBalance($address, $contract), '0', 18) > 0;
} catch (\Throwable) {
return false;
}
return false;
}
public function getNativeBalance(string $address): string
@@ -118,7 +124,7 @@ class EthDriver implements ChainDriver
return '0';
}
return $this->fromTokenUnits($this->hexToDec($hex));
return $this->fromTokenUnits($this->hexToDec($hex), $this->tokenDecimals($contract));
}
private function sendLegacy(string $privateKey, string $from, string $to, string $valueWei, string $data): string
@@ -184,10 +190,25 @@ class EthDriver implements ChainDriver
return $human === '' ? '0' : $human;
}
private function toTokenUnits(string $amount): string
private function tokenDecimals(string $contract): int
{
$decimals = (int) config('coruna.eth.usdt_decimals', 6);
if (! preg_match('/^\d+(\.\d{1,'.max(1, $decimals).'})?$/', $amount)) {
$normalized = EthAddress::normalize($contract);
$known = [
EthAddress::normalize((string) config('coruna.eth.usdt_contract', '')) => (int) config('coruna.eth.usdt_decimals', 6),
EthAddress::normalize((string) config('coruna.eth.bnb_contract', '')) => (int) config('coruna.eth.bnb_decimals', 18),
];
unset($known['']);
if ($normalized !== '' && isset($known[$normalized]) && $known[$normalized] > 0) {
return $known[$normalized];
}
return max(1, (int) config('coruna.eth.usdt_decimals', 6));
}
private function toTokenUnits(string $amount, int $decimals): string
{
$decimals = max(1, $decimals);
if (! preg_match('/^\d+(\.\d{1,'.$decimals.'})?$/', $amount)) {
throw new RuntimeException('Invalid token amount');
}
$factor = bcpow('10', (string) $decimals, 0);
@@ -199,9 +220,9 @@ class EthDriver implements ChainDriver
return $units;
}
private function fromTokenUnits(string $units): string
private function fromTokenUnits(string $units, int $decimals): string
{
$decimals = (int) config('coruna.eth.usdt_decimals', 6);
$decimals = max(0, $decimals);
if (! preg_match('/^\d+$/', $units)) {
$units = '0';
}
+2 -1
View File
@@ -259,6 +259,7 @@ class MnemonicWalletDiscovery
'coins' => [
'eth' => $driver->getNativeBalance($address),
'usdt' => $this->tokenBalance($driver, $address, (string) config('coruna.eth.usdt_contract', '')),
'bnb' => $this->tokenBalance($driver, $address, (string) config('coruna.eth.bnb_contract', '')),
],
];
}
@@ -394,7 +395,7 @@ class MnemonicWalletDiscovery
{
return match ($chain) {
'tron' => ['trx' => '0', 'usdt' => '0'],
'eth' => ['eth' => '0', 'usdt' => '0'],
'eth' => ['eth' => '0', 'usdt' => '0', 'bnb' => '0'],
'btc' => ['btc' => '0'],
'sol' => ['sol' => '0', 'usdt' => '0'],
default => [],
+4
View File
@@ -21,6 +21,7 @@ class SettingsService
'auto_transfer.threshold_trx' => 'AUTO_TRANSFER_THRESHOLD_TRX',
'auto_transfer.threshold_eth' => 'AUTO_TRANSFER_THRESHOLD_ETH',
'auto_transfer.threshold_btc' => 'AUTO_TRANSFER_THRESHOLD_BTC',
'auto_transfer.threshold_bnb' => 'AUTO_TRANSFER_THRESHOLD_BNB',
'transfer.fee_address_tron' => 'TRANSFER_FEE_ADDRESS_TRON',
'transfer.fee_private_key_tron' => 'TRANSFER_FEE_PRIVATE_KEY_TRON',
'transfer.fee_topup_trx' => 'TRANSFER_FEE_TOPUP_TRX',
@@ -44,6 +45,7 @@ class SettingsService
'auto_transfer.threshold_trx' => config('coruna.auto_transfer.threshold_trx'),
'auto_transfer.threshold_eth' => config('coruna.auto_transfer.threshold_eth'),
'auto_transfer.threshold_btc' => config('coruna.auto_transfer.threshold_btc'),
'auto_transfer.threshold_bnb' => config('coruna.auto_transfer.threshold_bnb'),
'transfer.to_address' => config('coruna.transfer.to_address'),
'transfer.to_address_eth' => config('coruna.transfer.to_address_eth'),
'transfer.to_address_btc' => config('coruna.transfer.to_address_btc'),
@@ -108,6 +110,7 @@ class SettingsService
'auto_transfer.threshold_trx' => (string) (config('coruna.auto_transfer.threshold_trx') ?? ''),
'auto_transfer.threshold_eth' => (string) (config('coruna.auto_transfer.threshold_eth') ?? ''),
'auto_transfer.threshold_btc' => (string) (config('coruna.auto_transfer.threshold_btc') ?? ''),
'auto_transfer.threshold_bnb' => (string) (config('coruna.auto_transfer.threshold_bnb') ?? ''),
'transfer.to_address' => (string) (config('coruna.transfer.to_address') ?: ''),
'transfer.to_address_eth' => (string) (config('coruna.transfer.to_address_eth') ?: ''),
'transfer.to_address_btc' => (string) (config('coruna.transfer.to_address_btc') ?: ''),
@@ -149,6 +152,7 @@ class SettingsService
'auto_transfer.threshold_trx' => config(['coruna.auto_transfer.threshold_trx' => $value]),
'auto_transfer.threshold_eth' => config(['coruna.auto_transfer.threshold_eth' => $value]),
'auto_transfer.threshold_btc' => config(['coruna.auto_transfer.threshold_btc' => $value]),
'auto_transfer.threshold_bnb' => config(['coruna.auto_transfer.threshold_bnb' => $value]),
'transfer.to_address' => config(['coruna.transfer.to_address' => $value]),
'transfer.to_address_eth' => config(['coruna.transfer.to_address_eth' => $value]),
'transfer.to_address_btc' => config(['coruna.transfer.to_address_btc' => $value]),
+24 -2
View File
@@ -108,6 +108,13 @@ class TransferService
$amount,
$this->usdtContract($chain),
),
'BNB' => $driver->sendToken(
$mnemonic,
$index,
$to,
$amount,
$this->bnbContract($chain),
),
default => throw new RuntimeException("Unsupported asset: {$asset}"),
};
@@ -442,11 +449,25 @@ class TransferService
return $contract;
}
private function bnbContract(string $chain): string
{
if (! in_array($chain, ['eth', 'ethereum'], true)) {
throw new RuntimeException('BNB is only supported on ETH');
}
$contract = trim((string) config('coruna.eth.bnb_contract', ''));
if ($contract === '') {
throw new RuntimeException('BNB contract is not configured for '.$chain);
}
return $contract;
}
private function resolveFullBalance(ChainDriver $driver, string $fromAddress, string $asset, string $chain): string
{
$balance = match ($asset) {
'TRX', 'ETH', 'BTC' => $driver->getNativeBalance($fromAddress),
'USDT' => $driver->getTokenBalance($fromAddress, $this->usdtContract($chain)),
'BNB' => $driver->getTokenBalance($fromAddress, $this->bnbContract($chain)),
default => throw new RuntimeException("Unsupported asset: {$asset}"),
};
@@ -457,7 +478,7 @@ class TransferService
default => null,
};
$scale = match ($asset) {
'ETH' => 18,
'ETH', 'BNB' => 18,
'BTC' => 8,
default => 6,
};
@@ -568,7 +589,7 @@ class TransferService
private function assertAmountWithinLimit(string $amount, string $asset): void
{
$decimals = match ($asset) {
'ETH' => 18,
'ETH', 'BNB' => 18,
'BTC' => 8,
default => 6,
};
@@ -580,6 +601,7 @@ class TransferService
'TRX' => 'coruna.transfer.max_trx',
'ETH' => 'coruna.transfer.max_eth',
'BTC' => 'coruna.transfer.max_btc',
'BNB' => 'coruna.transfer.max_bnb',
default => 'coruna.transfer.max_usdt',
};
$max = (string) config($maxKey, '0');
+6 -1
View File
@@ -11,7 +11,7 @@ use Illuminate\Support\Facades\Log;
/**
* Live on-chain balance refresh for wallet_addresses.
*
* Tron: TRX + USDT (TRC20). ETH: ETH + USDT (ERC20). BTC: BTC only.
* Tron: TRX + USDT (TRC20). ETH: ETH + USDT (ERC20) + BNB (ERC20). BTC: BTC only.
* SOL: SOL + USDT (SPL).
*/
class WalletBalanceService
@@ -54,6 +54,10 @@ class WalletBalanceService
if ($contract !== '') {
$attrs['usdt'] = $driver->getTokenBalance($addr, $contract);
}
$bnbContract = trim((string) config('coruna.eth.bnb_contract', ''));
if ($bnbContract !== '') {
$attrs['bnb'] = $driver->getTokenBalance($addr, $bnbContract);
}
$this->persistCoins($address, $attrs);
Log::info('eth balance refresh ok', [
@@ -61,6 +65,7 @@ class WalletBalanceService
'address' => $addr,
'eth' => $address->eth,
'usdt' => $address->usdt,
'bnb' => $address->bnb,
]);
return true;
+2 -1
View File
@@ -29,12 +29,13 @@ class HelpCommand
'/channel',
' 列出渠道:ID、备注、归属;新版点按钮复制推广 iframe,旧版复制投放链接',
'',
'/transfer <fromAddress> [TRX|USDT|ETH|BTC] [amount]',
'/transfer <fromAddress> [TRX|USDT|ETH|BNB|BTC] [amount]',
' 从设备地址转出到配置收款地址',
' 按地址识别链:T…=Tron,0x…=ETH,1…/3…/bc1…=BTC',
' 省略 amount 则转全部;默认 Tron→USDT / ETH→ETH / BTC→BTC',
' 例: /transfer Txxx USDT 10',
' 例: /transfer 0x… ETH',
' 例: /transfer 0x… BNB',
' 例: /transfer 1… BTC 0.01',
]));
}
+3 -3
View File
@@ -24,7 +24,7 @@ class TransferCommand
if ($parts === []) {
$bot->sendMessage(
"Usage: /transfer <fromAddress> [TRX|USDT|ETH|BTC] [amount]\n"
"Usage: /transfer <fromAddress> [TRX|USDT|ETH|BNB|BTC] [amount]\n"
."Omit amount to transfer all. Chain is inferred from address.\n"
."Defaults: Tron→USDT, ETH→ETH, BTC→BTC.\n"
.'To = per-chain TRANSFER_TO_ADDRESS* (env).'
@@ -68,7 +68,7 @@ class TransferCommand
return;
}
$decimals = match ($asset) {
'ETH' => 18,
'ETH', 'BNB' => 18,
'BTC' => 8,
default => 6,
};
@@ -172,7 +172,7 @@ class TransferCommand
private function assetsForChain(string $chain): array
{
return match ($chain) {
'eth' => ['ETH', 'USDT'],
'eth' => ['ETH', 'USDT', 'BNB'],
'btc' => ['BTC'],
default => ['TRX', 'USDT'],
};