PHP EIP712签名类 和 JS调用EIP712 方法
PHP-EIP712
安装必要的依赖
首先,通过 Composer 安装必要的依赖:
composer require kornrunner/keccak
composer require simplito/elliptic-php
编写 EIP712Signer 类
创建一个新的类 EIP712Signer,用于处理 EIP-712 签名生成。
<?php
namespace app\common\services;
use kornrunner\Keccak;
use Elliptic\EC;
class EIP712Signer
{
private $domain;
private $types;
private $primaryType;
public function __construct(array $domain, array $types, string $primaryType)
{
$this->domain = $domain;
$this->types = $types;
$this->primaryType = $primaryType;
}
public function sign(array $message, string $privateKey)
{
$domainSeparator = $this->hashStruct('EIP712Domain', $this->domain);
$messageHash = $this->hashStruct($this->primaryType, $message);
$dataToSign = $this->keccak256(hex2bin('1901' . $domainSeparator . $messageHash));
$ec = new EC('secp256k1');
$key = $ec->keyFromPrivate($privateKey);
$signature = $key->sign($dataToSign, ['canonical' => true]);
return [
'r' => '0x' . $signature->r->toString(16),
's' => '0x' . $signature->s->toString(16),
'v' => $signature->recoveryParam + 27
];
}
private function hashStruct(string $type, array $data)
{
$typeHash = $this->keccak256($this->encodeType($type));
$encodedData = $this->encodeData($type, $data);
return $this->keccak256(hex2bin($typeHash . $encodedData));
}
private function encodeType(string $type)
{
$types = $this->types[$type];
$encoded = $type . '(';
$encoded .= implode(',', array_map(function ($field) {
return $field['type'] . ' ' . $field['name'];
}, $types));
$encoded .= ')';
return $encoded;
}
private function encodeData(string $type, array $data)
{
$types = $this->types[$type];
$encodedValues = '';
foreach ($types as $field) {
$fieldType = $field['type'];
$fieldName = $field['name'];
$value = $data[$fieldName];
if (isset($this->types[$fieldType])) {
$encodedValues .= $this->hashStruct($fieldType, $value);
} else {
$encodedValues .= $this->encodeValue($fieldType, $value);
}
}
return $encodedValues;
}
private function encodeValue($type, $value)
{
switch ($type) {
case 'string':
case 'bytes':
return $this->keccak256($value);
case 'uint256':
case 'int256':
return str_pad(dechex($value), 64, '0', STR_PAD_LEFT);
case 'address':
return str_pad(substr($value, 2), 64, '0', STR_PAD_LEFT);
default:
throw new \InvalidArgumentException("Unsupported type: $type");
}
}
private function keccak256($value)
{
return Keccak::hash($value, 256);
}
private function strToHex($string)
{
return bin2hex($string);
}
}
使用示例
以下是如何使用 EIP712Signer 类生成符合 EIP-712 规范的签名的示例
public function test()
{
// 定义域
$domain = [
'name' => 'ClaimDomain',
'version' => '1',
'verifyingContract' => '验证的合约地址'
];
// 定义数据结构类型
$types = [
'EIP712Domain' => [
['name' => 'name', 'type' => 'string'],
['name' => 'version', 'type' => 'string'],
['name' => 'verifyingContract', 'type' => 'address']
],
'Withdrawal' => [
['name' => 'txId', 'type' => 'uint256'],
['name' => 'account', 'type' => 'address'],
['name' => 'amount', 'type' => 'uint256'],
['name' => 'deadline', 'type' => 'uint256']
]
];
// 要签名的数据
$message = [
'txId' => '订单号',
'account' => '账户',
'amount' => '数量', // 1 ETH in wei
'deadline' => '到期时间'// 1 hour from now
];
// 私钥(测试用,实际使用中应妥善保管)
$privateKey = '私钥';
$signer = new EIP712Signer($domain, $types, 'Withdrawal');
$signature = $signer->sign($message, $privateKey);
dd($signature);
}
JS-EIP712
node 版本
v16.20
需要的依赖 package.json
{
"name": "eip",
"version": "1.0.0",
"scripts": {
"install": "npm install"
},
"dependencies": {
"@metamask/eth-sig-util": "^6.0.0",
"ethereumjs-util": "^7.1.5"
}
}
eip712.js 方法
const ethSigUtils = require("@metamask/eth-sig-util");
const ethUtil = require("ethereumjs-util");
const SingData = () => {
const [privateStr, name, verifyingContract, txId, account, amount, deadline] = process.argv.slice(2);
const signParams = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "verifyingContract", type: "address" },
],
Withdrawal: [
{ name: "txId", type: "uint256" },
{ name: "account", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "Withdrawal",
domain: {
name,
version: "1",
verifyingContract,
},
message: {
txId,
account,
amount,
deadline,
},
};
// console.log(JSON.stringify(signParams))
const hash = ethSigUtils.TypedDataUtils.eip712Hash(
signParams,
ethSigUtils.SignTypedDataVersion.V4
);
const sign = ethUtil.ecsign(hash, ethUtil.toBuffer(privateStr));
return {
r: "0x" + sign.r.toString("hex"),
s: sign.s.toString("hex"),
v: sign.v,
};
};
console.log(JSON.stringify(SingData()));
调用方法
node index.js 签名私钥 ClaimDomain 合约地址 订单号 钱包地址 数量 时间
node index.js 0x738eb03f59351a1... ClaimDomain 0xd365d9D46c3AE4c58890dC9247... 1001010 0xb549aDa4a19f13213402F12d718b42895999C8e0 100000 1999999999
浏览器调用personal_sign 方法
await ethereum
.request({ method: 'personal_sign',params:['123','0x0211cFFf6e6266be70AAbF023a6E0b9284B95306'] })