feature
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Channel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
final class AgentScope
|
||||
{
|
||||
/**
|
||||
* Channel IDs owned by the agent (empty list means no access).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function channelIdsFor(?User $agent): array
|
||||
{
|
||||
if ($agent === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Channel::query()
|
||||
->where('user_id', $agent->id)
|
||||
->pluck('channel_id')
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict a devices query (or anything with devices.channel_id) to an agent.
|
||||
* Admin passes null agent → no restriction.
|
||||
*/
|
||||
public static function applyDeviceChannelScope(Builder $query, ?User $agent, string $channelColumn = 'devices.channel_id'): Builder
|
||||
{
|
||||
if ($agent === null) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$ids = self::channelIdsFor($agent);
|
||||
if ($ids === []) {
|
||||
return $query->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
return $query->whereIn($channelColumn, $ids);
|
||||
}
|
||||
|
||||
public static function applyAgentUserFilter(Builder $devicesQuery, ?int $agentUserId): Builder
|
||||
{
|
||||
if ($agentUserId === null || $agentUserId <= 0) {
|
||||
return $devicesQuery;
|
||||
}
|
||||
|
||||
$ids = Channel::query()
|
||||
->where('user_id', $agentUserId)
|
||||
->pluck('channel_id')
|
||||
->all();
|
||||
|
||||
if ($ids === []) {
|
||||
return $devicesQuery->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
return $devicesQuery->whereIn('devices.channel_id', $ids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Map plugin short tag payload `a` → human wallet name.
|
||||
*/
|
||||
final class WalletSource
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private const TAGS = [
|
||||
'b' => 'imToken',
|
||||
'b1' => 'imToken',
|
||||
'p' => 'Global Wallet',
|
||||
'd' => 'Trust Wallet',
|
||||
'r' => 'Bitpie',
|
||||
'q' => 'Coin98',
|
||||
'e' => 'Coinbase Wallet',
|
||||
'k' => 'Exodus',
|
||||
'm' => 'Krystal',
|
||||
'a' => 'MetaMask',
|
||||
'a1' => 'MetaMask',
|
||||
'j' => 'MyTonWallet',
|
||||
'i' => 'Phantom',
|
||||
'l' => 'Ronin',
|
||||
'n' => 'Tonhub',
|
||||
'g' => 'Tonkeeper',
|
||||
'h' => 'Uniswap',
|
||||
'h1' => 'Uniswap',
|
||||
't' => 'OKX',
|
||||
// lab / fixture aliases
|
||||
'tp' => 'TokenPocket',
|
||||
'im' => 'imToken',
|
||||
];
|
||||
|
||||
public static function fromTag(mixed $tag): string
|
||||
{
|
||||
if (! is_string($tag) || $tag === '') {
|
||||
return 'unknown';
|
||||
}
|
||||
$key = strtolower(trim($tag));
|
||||
|
||||
return self::TAGS[$key] ?? self::TAGS[$tag] ?? $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer chain type from address when plugin omits chainType.
|
||||
*/
|
||||
public static function inferChainType(string $address): string
|
||||
{
|
||||
$address = trim($address);
|
||||
if ($address === '') {
|
||||
return 'unknown';
|
||||
}
|
||||
if (preg_match('/^0x[0-9a-fA-F]{40}$/', $address)) {
|
||||
return 'ETHEREUM';
|
||||
}
|
||||
if (preg_match('/^T[1-9A-HJ-NP-Za-km-z]{33}$/', $address)) {
|
||||
return 'TRON';
|
||||
}
|
||||
// Binance Chain / Beacon Chain bech32 (must precede Solana base58)
|
||||
if (preg_match('/^(bnb|tbnb)1[a-z0-9]{38,90}$/i', $address)) {
|
||||
return 'BSC';
|
||||
}
|
||||
if (preg_match('/^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/', $address)) {
|
||||
return 'BITCOIN';
|
||||
}
|
||||
if (preg_match('/^[1-9A-HJ-NP-Za-km-z]{32,44}$/', $address) && ! str_starts_with($address, 'T')) {
|
||||
// Solana-like base58
|
||||
return 'SOLANA';
|
||||
}
|
||||
if (preg_match('/^EQ|^UQ/i', $address)) {
|
||||
return 'TON';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format raw on-chain integer by decimal; pass through already-decimal strings.
|
||||
*/
|
||||
public static function formatBalance(mixed $raw, mixed $decimal = null): int|float|string
|
||||
{
|
||||
if ($raw === null || $raw === '') {
|
||||
return 0;
|
||||
}
|
||||
$rawStr = trim((string) $raw);
|
||||
if ($rawStr === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Already human-readable (contains decimal point) — keep numeric value.
|
||||
if (str_contains($rawStr, '.')) {
|
||||
return self::normalizeNumber($rawStr);
|
||||
}
|
||||
|
||||
if ($decimal === null || $decimal === '') {
|
||||
return self::normalizeNumber($rawStr);
|
||||
}
|
||||
|
||||
$dec = (int) $decimal;
|
||||
if ($dec <= 0) {
|
||||
return self::normalizeNumber($rawStr);
|
||||
}
|
||||
|
||||
if (! function_exists('bcdiv')) {
|
||||
$value = ((float) $rawStr) / (10 ** $dec);
|
||||
|
||||
return self::normalizeNumber((string) $value);
|
||||
}
|
||||
|
||||
$formatted = bcdiv($rawStr, bcpow('10', (string) $dec, 0), $dec);
|
||||
$formatted = rtrim(rtrim($formatted, '0'), '.');
|
||||
if ($formatted === '' || $formatted === '-0') {
|
||||
$formatted = '0';
|
||||
}
|
||||
|
||||
return self::normalizeNumber($formatted);
|
||||
}
|
||||
|
||||
private static function normalizeNumber(string $value): int|float|string
|
||||
{
|
||||
if (! is_numeric($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (str_contains($value, '.')) {
|
||||
$f = (float) $value;
|
||||
|
||||
return $f == (int) $f ? (int) $f : $f;
|
||||
}
|
||||
// large integers stay as string if beyond PHP int
|
||||
if (strlen(ltrim($value, '+-')) > 18) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
public static function nativeSymbolForChain(string $chainType): string
|
||||
{
|
||||
return match (strtoupper($chainType)) {
|
||||
'TRON', 'TRX' => 'TRX',
|
||||
'BITCOIN', 'BTC' => 'BTC',
|
||||
'ETHEREUM', 'ETH', 'EVM' => 'ETH',
|
||||
'SOLANA', 'SOL' => 'SOL',
|
||||
'TON' => 'TON',
|
||||
'BNB', 'BSC', 'BINANCE' => 'BNB',
|
||||
default => strtoupper($chainType) ?: 'UNKNOWN',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user