最近在写ETH的NFT发行转账功能,使用的语言是PHP,但是发现github上使用比较多的web3.php有点问题,当solidity使用string[]类型时候web3.php没有做兼容,最后会导致签名后的数据有问题,交易出现 Warning! Error encountered during contract execution [execution reverted] ,修改后特意来记录一下。
composer.php:
{
"require": {
"sc0vu/web3.php": "dev-master",
"web3p/ethereum-tx": "dev-master",
"simplito/elliptic-php": "~1.0.4",
"kornrunner/keccak": "~1.0",
"graze/guzzle-jsonrpc": "^3.2",
"bitwasp/buffertools": "^0.5.0"
}
}
IPFSapi和ETHapi都是使用的infura:
php上传文件到IPFS可以参考前面的文章,ETH的原生签名交易也可以参考前面的文章。
web3实现ETH ERC20、ERC721签名时候data数据的拼装,封装了一个类可以参考一下:
<php
/**
* Created by PhpStorm.
* User: Echo
* Date: 2021/8/24
* Time: 5:07 PM
*/
use EthTool\\Credential;
use EthTool\\EthInfuraApi;
use Web3\\Contract;
use Web3\\Utils;
use EthTool\\EthApi;
use Web3\\Contracts\\Ethabi;
use Web3\\Contracts\\Types\\Address;
use Web3\\Contracts\\Types\\Boolean;
use Web3\\Contracts\\Types\\Bytes;
use Web3\\Contracts\\Types\\DynamicBytes;
use Web3\\Contracts\\Types\\Integer;
use Web3\\Contracts\\Types\\Str;
use Web3\\Contracts\\Types\\Uinteger;
use IPFS\\IPFS;
use Web3\\Web3;
class Nft{
private $abi= '你的ABI'; //abi
private $contract_address = '';
private $key = "";
private $self_address = "";
private $credential;
private $eth_host = "";
private $api_key = "";
private $ethabi;
private $eth_api;
private $address_key="address:nonce:key:";
/**
* Nft constructor.
*/
public function __construct(){
$eth_config = config("myconfig.ETH");
$this->contract_address = $eth_config["contract_address"];
$this->key = $eth_config["my_key"];
$this->credential = Credential::fromKey($this->key);
$this->self_address = $this->credential->getAddress();
$this->eth_host = $eth_config["api_host"];
$this->api_key = $eth_config["api_key"];
$this->eth_api = new EthInfuraApi($this->eth_host,$this->api_key);
$this->ethabi = new Ethabi([
'address' => new Address,
'bool' => new Boolean,
'bytes' => new Bytes,
'dynamicBytes' => new DynamicBytes,
'int' => new Integer,
'string' => new Str,
'uint' => new Uinteger,
]);
}
/**
* 发行NFT
* @param $name string NFT名称
* @param $description string NFT介绍
* @param $img_url string NFT图片在阿里云的链接
* @param int $number 要发行的个数
* @return array|bool|int|mixed|null|string
* status: -1为失败 1成功
* ipfs_img_url_hash: 图片上传到IPFS的hash
* ipfs_nft_info_url_hash: NFT信息上传到IPFS的hash
*/
public function createNft($name,$description,$img_url,$number=1){
$number = (int)$number;
if (!$name || !$description || !$img_url || $number < 1){
return [
"status" => -1,
"msg" => "参数格式有误"
];
}
$return_info = [
"name" => $name,
"description" => $description,
"img_url" => $img_url,
"mint_hash" => "",
"nft_info" => []
];
$img_url_hash = $this->uploadPhoto($img_url);
if (is_array($img_url_hash) && isset($img_url_hash["status"])){
return $img_url_hash;
}
$info_url = $this->uploadData($img_url_hash,$name,$description);
if (is_array($info_url) && isset($info_url["status"])){
return $info_url;
}
$new_token_id =$this->getNewTokenId($this->self_address,$info_url);
if (is_array($new_token_id) && isset($new_token_id["status"])){
return $new_token_id;
}
if ($number == 1){
$mint_hash = $this->mintNft($this->self_address, $info_url);
if (is_array($mint_hash) && isset($mint_hash["status"])){
return $mint_hash;
}
array_push($return_info["nft_info"],
[
"ipfs_img_url_hash" => $img_url_hash,
"ipfs_nft_info_url_hash" => $info_url,
"nft_id" => $new_token_id
]
);
}else{
$address_array = [];
$url_array = [];
for ($i=1;$i<=$number;$i++){
$address_array[] = $this->self_address;
$url_array[] = $info_url;
array_push($return_info["nft_info"],
[
"ipfs_img_url_hash" => $img_url_hash,
"ipfs_nft_info_url_hash" => $info_url,
"nft_id" => $new_token_id
]
);
$new_token_id += 1;
}
$mint_hash = $this->mintArrayNft($address_array, $url_array);
if (is_array($mint_hash) && isset($mint_hash["status"])){
return $mint_hash;
}
}
$return_info["mint_hash"] = $mint_hash;
return [
"status" => 1,
"msg" => "成功",
"data" => $return_info
];
}
/**
* 生成新的地址
* @return array
*/
public function createAddress(){
try{
$key = Credential::newWallet();
$credential = Credential::fromKey($key);
return $data = [
'private' => $credential->getPrivateKey(),
'public' => $credential->getPublicKey(),
'address' => $credential->getAddress()
];
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
/**
* 查询地址中NFT的数量
* @param $address
* @return array|bool|mixed|string
*/
public function nftBalance($address){
try{
$param_data = $this->ethabi->encodeParameter('address', $address);
$param_data = Utils::stripZero($param_data);
$method_id = $this->ethabi->encodeFunctionSignature("balanceOf(address)");
$number = $this->eth_api->getCall($this->self_address, $this->contract_address, "0x0", $method_id . $param_data);
// $number = Utils::stripZero($number);
$number = Utils::toBn($number)->toString();
return $number;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
/**
* 根据tokenID返回持有者地址
* @param $id
* @return array|bool|mixed|string
*/
public function getAddressByTokenId($id){
try{
$param_data = $this->ethabi->encodeParameter('uint256', $id);
$param_data = Utils::stripZero($param_data);
$method_id = $this->ethabi->encodeFunctionSignature("ownerOf(uint256)");
$address = $this->eth_api->getCall($this->self_address, $this->contract_address, "0x0", $method_id . $param_data);
$address = $this->ethabi->decodeParameter('address', $address);
return $address;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
/**
* 根据TokenID返回Token的URL信息
* @param $id
* @return array|bool|mixed|string
*/
public function getUrlByTokenId($id){
try{
$param_data = $this->ethabi->encodeParameter('uint256', $id);
$param_data = Utils::stripZero($param_data);
$method_id = $this->ethabi->encodeFunctionSignature("tokenURI(uint256)");
$url = $this->eth_api->getCall($this->self_address, $this->contract_address, "0x0", $method_id . $param_data);
$url = $this->ethabi->decodeParameter('string', $url);
return $url;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
/**
* 返回最新的NFT的ID(预铸造、本地计数使用)
* @param $to_address
* @param $nft_url
* @return array|bool|mixed|string
*/
public function getNewTokenId($to_address,$nft_url){
try{
$param_data = $this->ethabi->encodeParameters(
['address','string'],
[$to_address,"ipfs://".$nft_url]
);
$param_data = Utils::stripZero($param_data);
$method_id = $this->ethabi->encodeFunctionSignature("mint(address,string)");
$number = $this->eth_api->getCall($this->self_address, $this->contract_address, "0x0", $method_id . $param_data);
$number = Utils::toBn($number)->toString();
return $number;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
/**
* 铸造一个NFT
* @param $to_address address 发布到的地址
* @param $nft_url string NFT的信息URL
* @return array|bool|mixed
*/
public function mintNft($to_address,$nft_url){
try{
$param_data = $this->ethabi->encodeParameters(
['address','string'],
[$to_address,"ipfs://".$nft_url]
);
$param_data = Utils::stripZero($param_data);
$method_id = $this->ethabi->encodeFunctionSignature("mint(address,string)");
$address_key = $this->address_key.$this->self_address;
getRedis()->del($address_key);
$nonce_num = getRedis()->get($address_key);
if (!$nonce_num){
$nonce_num = $this->getNonce($this->self_address);
}
$nonce = Utils::toHex($nonce_num,true);
$gas_limit = $this->eth_api->getEstimateGas($this->self_address,$this->contract_address,"0x0",$method_id . $param_data);
$gasprice = $this->eth_api->getGasPrice();
$data = [
'nonce' => $nonce,
'gasPrice' => $gasprice,
'gasLimit' => $gas_limit, //16进制
'to' => $this->contract_address, //代币地址
'value' => '0x0',
//substr(Utils::sha3("mint(address,string)",true),0,10) == 0xd0def521
'data' => $method_id . $param_data,
// 'chainId' => 80001,
'chainId' => 137
];
$signed = $this->credential->signTransaction($data); // 进行离线签名
$hash = $this->eth_api->sendRawTransaction($signed);
getRedis()->setex($address_key,43200,$nonce_num+1);
return $hash;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
public function web3Test($address,$url){
// $web3 = new Web3("https://rpc-mumbai.maticvigil.com/");
$web3 = new Web3($this->eth_host.$this->api_key);
$contract = new Contract($web3->provider, $this->abi);
$data_aaa = $contract->at($this->contract_address)->getData('mintArray',$address,$url);
return $data_aaa;
}
/**
* @param $to_address array 发布到的地址
* @param $nft_url array NFT的信息URL
* @return array|bool|mixed
*/
public function mintArrayNft($to_address,$nft_url){
if (count($to_address) !== count($nft_url) || count($to_address) < 1){
return [
"status" => -1,
"msg" => "地址数和NFT信息数不同"
];
}
try{
foreach ($nft_url as &$one_url){
$one_url = "ipfs://".$one_url;
}
$param_data = $this->ethabi->encodeParameters(
['address[]','string[]'],
[$to_address,$nft_url]
);
$param_data = Utils::stripZero($param_data);
// var_dump($param_data);
// $web3_data = $this->web3Test($to_address,$nft_url);
$method_id = $this->ethabi->encodeFunctionSignature("mintArray(address[],string[])");
$address_key = $this->address_key.$this->self_address;
getRedis()->del($address_key);
$nonce_num = getRedis()->get($address_key);
if (!$nonce_num){
$nonce_num = $this->getNonce($this->self_address);
}
$nonce = Utils::toHex($nonce_num,true);
$gas_limit = $this->eth_api->getEstimateGas($this->self_address,$this->contract_address,"0x0",$method_id . $param_data);
$gasprice = $this->eth_api->getGasPrice();
$data = [
'nonce' => $nonce,
// 'gasPrice' => '0x' . Utils::toWei("8", 'gwei')->toHex(),
'gasPrice' => $gasprice,
'gasLimit' => $gas_limit, //16进制
// 'gasLimit' => "0x61a80", //16进制
'to' => $this->contract_address, //代币地址
'value' => '0x0',
//substr(Utils::sha3("mint(address,string)",true),0,10) == 0xd0def521
'data' => $method_id . $param_data,
// 'chainId' => 80001,
'chainId' => 137
];
// var_dump($data);
$signed = $this->credential->signTransaction($data); // 进行离线签名
$hash = $this->eth_api->sendRawTransaction($signed);
getRedis()->setex($address_key,43200,$nonce_num+1);
return $hash;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
public function transferNft($to_address,$nft_id){
//Todo
}
public function transferMatic($to_address,$number){
//Todo
}
/**
* 拿到地址交易的nonce值
* @param $address
* @return bool|mixed|string
* @throws \\Exception
*/
protected function getNonce($address){
$nonce_num = $this->eth_api->getTransactionCount($address);
if ($nonce_num == false){
return false;
}
$nonce_num = Utils::toBn($nonce_num)->toString();
return $nonce_num;
}
/**
* 往IPFS上传图片
* @param $url string 图片的远程链接
* @return array|mixed|null
*/
public function uploadPhoto($url){
try{
$ipfs = new IPFS();
$hash = $ipfs->addFromUrl($url);
// var_dump($ipfs->pinAdd($hash));
return $hash;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
/**
* 根据图片的链接生成NFT介绍信息并上传到IPFS
* @param $url string 图片在IPFS的链接
* @param $name string NFT名称
* @param $description string NFT介绍
* @return array|null|string
*/
public function uploadData($url,$name,$description){
try{
$ipfs = new IPFS();
$data = [
"name" => $name,
"description" => $description,
"image" => "ipfs://".$url
];
$hash = $ipfs->add(json_encode($data));
return $hash;
}catch (\\Exception $e){
return [
"status" => $e->getCode(),
"msg" => $e->getMessage()
];
}
}
}
web3.php有问题的代码主要为encodeParameters()方法,也就是对data数据需要的方法和参数进行转换时候。
修改的具体文件为:vendor/sc0vu/web3.php/src/Contracts/SolidityType.php encode
方法,下面为我修改后的代码:
/**
* encode
*
* @param mixed $value
* @param string $name
* @return string
*/
public function encode($value, $name)
{
if ($this->isDynamicArray($name)) {
$length = count($value);
$nestedName = $this->nestedName($name);
$result = [];
$result[] = IntegerFormatter::format($length);
if ($this->isDynamicType($nestedName)){
$start = 0;
foreach ($value as $k => $val) {
if ($start == 0){
$l = $length * 32;
}else{
$v_1 = Utils::toHex($value[$k-1]);
$l = (floor((mb_strlen($v_1) + 63) / 64)+1) * 32;
}
$start += $l;
$result[] = IntegerFormatter::format($start);
}
//var_dump($result);
// die();
}
foreach ($value as $val) {
$result[] = $this->encode($val, $nestedName);
}
return $result;
} elseif ($this->isStaticArray($name)) {
$length = $this->staticArrayLength($name);
$nestedName = $this->nestedName($name);
$result = [];
foreach ($value as $val) {
$result[] = $this->encode($val, $nestedName);
}
return $result;
}
return $this->inputFormat($value, $name);
}
之所以这么改是因为官方说string是动态元素,所以要加上偏移量,原话如下:
since strings are dynamic elements we need to find their offsets c
, d
and e
:
参考链接:https://docs.soliditylang.org/en/latest/abi-spec.html#use-of-dynamic-types