PHP实现ETH ERC20签名交易

最近在写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:

https://infura.io/

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

猜你喜欢

转载自blog.csdn.net/qq_36228377/article/details/123154439
今日推荐