83 lines
2.6 KiB
PHP
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);
|
|
}
|
|
}
|