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

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
要用PHP遍历以太坊区块并识别ERC721交易,你可以使用以太坊客户端库web3.phpERC721智能合约的ABI(Application Binary Interface)。 以下是示例代码: ```php // 引入web3.php库 require_once('vendor/autoload.php'); // 连接以太坊节点 $web3 = new \Web3\Web3(new \Web3\Providers\HttpProvider('http://localhost:8545')); // 获取ERC721智能合约的ABI $erc721ABI = json_decode(file_get_contents('erc721_abi.json'), true); // 获取ERC721智能合约的地址 $erc721Address = '0x...'; // 创建ERC721智能合约的实例 $erc721 = new \Web3\Contract($web3->provider, $erc721ABI); $erc721->at($erc721Address); // 获取区块链上最新的区块号 $latestBlockNumber = $web3->eth->getBlockNumber(); // 遍历区块链上的所有区块 for ($i = 0; $i <= $latestBlockNumber; $i++) { // 获取区块信息 $block = $web3->eth->getBlockByNumber($i, true); // 遍历区块中的所有交易 foreach ($block->transactions as $transaction) { // 检查交易是否符合ERC721规范 if (preg_match('/^0x23b872dd(.{64})(.{64})(.{64})(.{40})(.{40})/', $transaction->input, $matches)) { // 解析交易数据 $tokenId = hexdec($matches[1]); $to = $matches[4]; $from = $matches[5]; // 调用ERC721智能合约的ownerOf方法获取tokenId的拥有者 $owner = $erc721->call('ownerOf', $tokenId); // 输出交易信息 echo "交易哈希值:{$transaction->hash}\n"; echo "交易发送方:{$from}\n"; echo "交易接收方:{$to}\n"; echo "转移的ERC721代币ID:{$tokenId}\n"; echo "ERC721代币的拥有者:{$owner}\n\n"; } } } ``` 在上面的示例代码中,我们使用正则表达式检查交易数据是否符合ERC721规范,如果符合则解析交易数据并调用ERC721智能合约的ownerOf方法获取ERC721代币的拥有者。根据实际情况,你可能需要对代码进行一些修改,以适应你的应用场景。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值