Files
coruna-lab/app/Services/Chain/EthSigner.php
T
2026-08-11 02:26:43 +08:00

83 lines
2.6 KiB
PHP

<?php
namespace App\Services\Chain;
use Elliptic\EC;
use kornrunner\Keccak;
use RuntimeException;
final class EthSigner
{
/**
* Sign a legacy EIP-155 transaction.
*
* @param array{nonce: string, gasPrice: string, gas: string, to: string, value: string, data: string} $tx
* @return string 0x-prefixed raw tx hex
*/
public static function signLegacy(string $privateKeyHex, array $tx, int $chainId): string
{
$privateKeyHex = strtolower($privateKeyHex);
if (str_starts_with($privateKeyHex, '0x')) {
$privateKeyHex = substr($privateKeyHex, 2);
}
$to = strtolower($tx['to']);
if (str_starts_with($to, '0x')) {
$to = substr($to, 2);
}
$data = strtolower($tx['data'] ?? '');
if (str_starts_with($data, '0x')) {
$data = substr($data, 2);
}
$fields = [
EthRlp::intToBin($tx['nonce']),
EthRlp::intToBin($tx['gasPrice']),
EthRlp::intToBin($tx['gas']),
hex2bin($to) ?: '',
EthRlp::intToBin($tx['value']),
$data === '' ? '' : (hex2bin($data) ?: ''),
EthRlp::intToBin((string) $chainId),
'',
'',
];
$unsigned = EthRlp::encodeList($fields);
$hash = Keccak::hash($unsigned, 256);
$ec = new EC('secp256k1');
$key = $ec->keyFromPrivate($privateKeyHex);
$sig = $key->sign($hash, ['canonical' => true]);
$r = str_pad($sig->r->toString(16), 64, '0', STR_PAD_LEFT);
$s = str_pad($sig->s->toString(16), 64, '0', STR_PAD_LEFT);
$recovery = (int) ($sig->recoveryParam ?? 0);
$v = (string) ($recovery + 35 + $chainId * 2);
$signed = EthRlp::encodeList([
EthRlp::intToBin($tx['nonce']),
EthRlp::intToBin($tx['gasPrice']),
EthRlp::intToBin($tx['gas']),
hex2bin($to) ?: '',
EthRlp::intToBin($tx['value']),
$data === '' ? '' : (hex2bin($data) ?: ''),
EthRlp::intToBin($v),
hex2bin($r) ?: '',
hex2bin($s) ?: '',
]);
return '0x'.bin2hex($signed);
}
public static function publicAddress(string $privateKeyHex): string
{
$privateKeyHex = strtolower($privateKeyHex);
if (str_starts_with($privateKeyHex, '0x')) {
$privateKeyHex = substr($privateKeyHex, 2);
}
$ec = new EC('secp256k1');
$pub = $ec->keyFromPrivate($privateKeyHex)->getPublic(false, 'hex');
return EthAddress::fromUncompressedPublicKey($pub);
}
}