104 lines
3.2 KiB
PHP
104 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Chain;
|
|
|
|
use kornrunner\Keccak;
|
|
use RuntimeException;
|
|
|
|
final class TronAddress
|
|
{
|
|
public static function fromUncompressedPublicKey(string $publicKeyHex): string
|
|
{
|
|
$hex = strtolower($publicKeyHex);
|
|
if (str_starts_with($hex, '0x')) {
|
|
$hex = substr($hex, 2);
|
|
}
|
|
if (str_starts_with($hex, '04')) {
|
|
$hex = substr($hex, 2);
|
|
}
|
|
if (strlen($hex) !== 128) {
|
|
throw new RuntimeException('Expected uncompressed secp256k1 public key');
|
|
}
|
|
|
|
$hash = Keccak::hash(hex2bin($hex), 256);
|
|
$addrHex = '41'.substr($hash, -40);
|
|
|
|
return self::hexToBase58Check($addrHex);
|
|
}
|
|
|
|
public static function isValid(string $address): bool
|
|
{
|
|
if (! preg_match('/^T[1-9A-HJ-NP-Za-km-z]{33}$/', $address)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$hex = self::base58CheckToHex($address);
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
|
|
return str_starts_with(strtolower($hex), '41') && strlen($hex) === 42;
|
|
}
|
|
|
|
public static function toHex(string $base58): string
|
|
{
|
|
return self::base58CheckToHex($base58);
|
|
}
|
|
|
|
public static function hexToBase58Check(string $hex): string
|
|
{
|
|
$alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
$bin = hex2bin($hex);
|
|
if ($bin === false) {
|
|
throw new RuntimeException('Invalid hex');
|
|
}
|
|
$checksum = substr(hash('sha256', hash('sha256', $bin, true), true), 0, 4);
|
|
$bytes = $bin.$checksum;
|
|
$num = gmp_init(bin2hex($bytes), 16);
|
|
$encoded = '';
|
|
while (gmp_cmp($num, 0) > 0) {
|
|
[$num, $rem] = [gmp_div_q($num, 58), gmp_intval(gmp_mod($num, 58))];
|
|
$encoded = $alphabet[$rem].$encoded;
|
|
}
|
|
for ($i = 0, $len = strlen($bytes); $i < $len && $bytes[$i] === "\x00"; $i++) {
|
|
$encoded = '1'.$encoded;
|
|
}
|
|
|
|
return $encoded;
|
|
}
|
|
|
|
public static function base58CheckToHex(string $address): string
|
|
{
|
|
$alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
$num = gmp_init(0);
|
|
for ($i = 0, $len = strlen($address); $i < $len; $i++) {
|
|
$pos = strpos($alphabet, $address[$i]);
|
|
if ($pos === false) {
|
|
throw new RuntimeException('Invalid base58 character');
|
|
}
|
|
$num = gmp_add(gmp_mul($num, 58), $pos);
|
|
}
|
|
$hex = gmp_strval($num, 16);
|
|
if (strlen($hex) % 2 !== 0) {
|
|
$hex = '0'.$hex;
|
|
}
|
|
// leading ones => leading zero bytes
|
|
for ($i = 0, $len = strlen($address); $i < $len && $address[$i] === '1'; $i++) {
|
|
$hex = '00'.$hex;
|
|
}
|
|
$bin = hex2bin($hex);
|
|
if ($bin === false || strlen($bin) < 5) {
|
|
throw new RuntimeException('Invalid address payload');
|
|
}
|
|
$payload = substr($bin, 0, -4);
|
|
$checksum = substr($bin, -4);
|
|
$expected = substr(hash('sha256', hash('sha256', $payload, true), true), 0, 4);
|
|
if (! hash_equals($expected, $checksum)) {
|
|
throw new RuntimeException('Invalid address checksum');
|
|
}
|
|
|
|
return bin2hex($payload);
|
|
}
|
|
}
|