PHP项目以太坊与Web3

wen PHP项目 2

本文目录导读:

PHP项目以太坊与Web3

  1. 核心方案对比
  2. 方案一:使用 web3.php (sc0Vu 版本) — 最推荐
  3. 方案二:使用直接cURL调用JSON-RPC(快速演示/简单查询)
  4. 安全与最佳实践
  5. 总结推荐

这是一个关于 PHP项目整合以太坊与Web3 的全面指南。

在PHP中与以太坊区块链交互,没有像JavaScript(Ethers.js, Web3.js)那样官方或流行的原生库,常见的做法是通过 HTTP JSON-RPC 直接与以太坊节点(如Geth, Infura)通信,或者使用一些封装好的PHP库。

下面将详细介绍几种主流方案、核心操作示例以及关键注意事项。

核心方案对比

方案 优点 缺点 适用场景
直接使用 Guzzle/cURL 调用 JSON-RPC 无依赖,完全可控,理解底层原理 代码量大,需要手动处理ABI编码/解码(非常复杂) 简单的查询(如获取余额、Gas价格)
使用 web3.php (sc0Vu 版本) 社区最流行,功能较全,封装了RPC调用 ABI编码仍需额外库(如 web3p/ethereum-tx),文档不算极佳 大多数应用场景,需要发送交易、调用合约
使用 web3.php (iexbase 版本) 功能简洁,直接 更新不够活跃,功能相对少 简单合约调用
使用 Ethereum.php 集成了ABI编码与签名,方便发送交易 对较新的以太坊特性支持可能滞后 需要快速实现交易发送的项目
使用 Laravel-Web3 (包) 与Laravel框架深度集成,支持门面(Facade) 仅限Laravel项目 Laravel项目

使用 web3.php (sc0Vu 版本) — 最推荐

这个库是目前PHP社区最活跃、功能最完善的以太坊库。

安装

composer require sc0vu/web3.php

配置连接(连接到 Infura 或本地节点)

<?php
require_once 'vendor/autoload.php';
use Web3\Web3;
use Web3\Providers\HttpProvider;
use Web3\RequestManagers\HttpRequestManager;
// 1. 连接到以太坊节点(可以是本地Geth,或Infura)
$rpcUrl = 'https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID'; // 使用Infura
// $rpcUrl = 'http://127.0.0.1:8545'; // 本地Ganache/Geth
$web3 = new Web3(new HttpProvider(new HttpRequestManager($rpcUrl, 30)));
echo "连接成功! \n";

基本操作:获取余额与Gas价格

<?php
use Web3\Eth;
use Web3\Utils;
$eth = new Eth($web3->provider);
// 获取地址余额(单位:Wei)
$address = '0xYourAddressHere';
$eth->getBalance($address, function ($err, $balance) {
    if ($err !== null) {
        echo 'Error: ' . $err->getMessage();
        return;
    }
    // 结果为Bignumber对象,需要转换为以太
    $ethBalance = Utils::fromWei($balance->toString(), 'ether');
    echo '余额: ' . $ethBalance . ' ETH' . PHP_EOL;
});
// 获取当前Gas价格(Wei)
$eth->gasPrice(function ($err, $gasPrice) {
    if ($err !== null) {
        echo 'Error: ' . $err->getMessage();
        return;
    }
    $gasPriceEth = Utils::fromWei($gasPrice->toString(), 'gwei');
    echo 'Gas价格: ' . $gasPriceEth . ' Gwei' . PHP_EOL;
});

调用智能合约的 view / pure 函数(读取数据,不需要Gas)

<?php
use Web3\Contract;
// 合约地址
$contractAddress = '0xYourContractAddress';
// 合约ABI (从编译后的JSON中获取)
$abi = '[
    {
        "constant": true,
        "inputs": [],
        "name": "totalSupply",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [{"name": "_owner", "type": "address"}],
        "name": "balanceOf",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function"
    }
]';
$contract = new Contract($web3->provider, $abi);
// 调用totalSupply
$contract->at($contractAddress)->call('totalSupply', function ($err, $result) {
    if ($err !== null) {
        echo 'Error: ' . $err->getMessage();
        return;
    }
    // $result 是一个数组,第一个元素是返回值
    echo 'Total Supply (Wei): ' . $result[0]->toString() . PHP_EOL;
});
// 调用balanceOf
$ownerAddress = '0xSomeOwnerAddress';
$contract->at($contractAddress)->call('balanceOf', $ownerAddress, function ($err, $result) {
    if ($err !== null) {
        echo 'Error: ' . $err->getMessage();
        return;
    }
    echo 'Balance (Wei): ' . $result[0]->toString() . PHP_EOL;
});

发送交易(写合约或转账ETH)- 核心难点

发送交易需要签名,这意味着你需要使用私钥。绝对不要在服务器代码中硬编码私钥! 应该使用环境变量或安全的密钥管理服务。

这里需要额外的库来处理签名:

composer require web3p/ethereum-tx
composer require web3p/ethereum-util

示例:转账ETH

<?php
use Web3\Utils;
use Web3p\EthereumTx\Transaction;
use Web3p\EthereumUtil\Util;
// 1. 准备参数
$fromPrivateKey = '0xYOUR_PRIVATE_KEY'; // 绝对不要硬编码!使用环境变量
$toAddress = '0xRecipientAddress';
$amountInEther = '0.01';
// 2. 获取Nonce(交易计数)
$eth = new Eth($web3->provider);
$fromAddress = (new Util())->privateKeyToPublicKey($fromPrivateKey);
$fromAddress = (new Util())->publicKeyToAddress($fromAddress); // 转换为地址
$eth->getTransactionCount($fromAddress, 'pending', function ($err, $nonce) use ($eth, $fromPrivateKey, $toAddress, $amountInEther) {
    if ($err !== null) { /* 处理错误 */ }
    // 3. 获取Gas价格
    $eth->gasPrice(function ($err, $gasPrice) use ($nonce, $fromPrivateKey, $toAddress, $amountInEther, $eth) {
        if ($err !== null) { /* 处理错误 */ }
        // 4. 构建交易体
        $transaction = new Transaction([
            'nonce' => '0x' . dechex($nonce->toString()),
            'from' => $fromAddress,
            'to' => '0x' . $toAddress, // 去掉地址的0x前缀?看库要求
            'gas' => '0x' . dechex(21000), // 简单的ETH转账Gas限制为21000
            'gasPrice' => '0x' . $gasPrice->toHex(),
            'value' => '0x' . Utils::toWei($amountInEther, 'ether')->toHex(),
            'chainId' => 1 // 主网:1, Ropsten:3, Rinkeby:4, 本地Ganache:1337
        ]);
        // 5. 使用私钥签名
        $transaction->sign($fromPrivateKey);
        // 6. 发送原始交易
        $eth->sendRawTransaction('0x' . $transaction->serialize(), function ($err, $txHash) {
            if ($err !== null) {
                echo '发送失败: ' . $err->getMessage();
                return;
            }
            echo '交易Hash: ' . $txHash . PHP_EOL;
        });
    });
});

注意:以上代码省略了dechex可能对大数处理的错误,实际生产环境需使用BCMath或GMP库处理大整数。ethereum-tx库内部会处理大数,但你自己构建Hex时要注意。

发送调用合约的函数(写操作)

与上述类似,但需要:

  • 构建交易时,to 字段为合约地址。
  • value 字段通常为 0x0(除非是payable函数)。
  • data 字段必须是从ABI编码后的函数调用数据(包括函数选择器和参数)。
  • gas 字段需要估算(使用 eth_estimateGas,但手动估算不准确,建议使用库的估算功能)。

web3.phpContract类提供了send方法简化此过程(但底层仍需签名):

$contract->at($contractAddress)->send('transfer', $toAddress, $amount, [
    'from' => $fromAddress,
    'gas' => '0x200b2', // 可以不用设置gas,让库估算?但需要签名上下文
], function ($err, $result) {
    // 返回的是交易Hash
});

签名上下文是缺失的,你需要将签名逻辑封装起来,或者在调用send之前设置好签名器。web3.php本身不管理私钥,这是为了安全,你可以使用 \Web3p\EthereumTx\Transaction 手动签名后,用 sendRawTransaction 发送。

使用直接cURL调用JSON-RPC(快速演示/简单查询)

如果只是做非常简单的查询(如总供应量),可以不依赖任何库。

function eth_call($rpcUrl, $contractAddress, $data) {
    $payload = json_encode([
        'jsonrpc' => '2.0',
        'method' => 'eth_call',
        'params' => [
            ['to' => $contractAddress, 'data' => $data],
            'latest'
        ],
        'id' => 1
    ]);
    $ch = curl_init($rpcUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    $result = curl_exec($ch);
    curl_close($ch);
    $response = json_decode($result, true);
    return $response['result'] ?? null;
}
// 示例:获取ERC20代币名称 (函数选择器: 0x06fdde03)
$data = '0x06fdde03'; // 没有参数
$result = eth_call($rpcUrl, '0xTokenContractAddress', $data);
// 结果是一个十六进制字符串,需要解码
echo 'Name (hex): ' . $result;

缺点:对于有参数的函数,你必须自己手动进行ABI编码(将地址、uint等转换成32字节的十六进制),这非常容易出错且繁琐,复杂操作不推荐此方法。

安全与最佳实践

  1. 私钥安全: 永远不要将私钥硬编码在代码中或提交到Git仓库,使用 .env 文件、环境变量、云端密钥管理服务(如AWS KMS, HashiCorp Vault)。
  2. 错误处理: 所有rpc调用都是异步回调或返回Promise(对于某些库),必须实现健壮的错误处理(网络超时、节点错误、Gas不足、Nonce冲突等)。
  3. Nonce管理: 对于需要发送多个交易的应用,Nonce必须严格递增,如果并发发送,需要实现Nonce锁定机制,否则容易出现“Nonce too low”错误。
  4. Gas估算: 总是使用 eth_estimateGas 来估算Gas,但注意它可能不准确(尤其是合约内部有复杂逻辑),建议加上Buffer(例如估算值 * 1.2)。
  5. 链ID: 签名交易时必须指定正确的 chainId (主网:1, 等等),否则交易会在错误的链上重放。
  6. 使用Bignumber: PHP原生整数对于Wei(18位小数)会溢出,必须使用BCMath或GMP扩展,或者依赖库内部的处理。web3.php 返回的结果通常是 phpseclib3\Math\BigInteger 对象。

总结推荐

  • 如果你是Laravel项目:可以尝试 Laravel-Web3 包,它封装得很好。
  • 如果你是纯PHP或ThinkPHP等:使用 sc0vu/web3.php 搭配 web3p/ethereum-tx
    • Contract 类调用 view 函数。
    • Transaction 类手动构建、签名交易,用 sendRawTransaction 发送。
  • 只在查询余额/总供应量等简单场景:用cURL直接调用JSON-RPC,但不要用于复杂的写操作。

下一步建议:

  1. 安装 sc0vu/web3.phpweb3p/ethereum-tx
  2. 连接到Sepolia测试网(而不是主网)进行开发测试。
  3. 申请Infura或Alchemy的API Key。
  4. 编写一个简单的脚本:查询自己测试地址的余额。
  5. 编写一个脚本:调用一个简单的测试合约(如Counter)的 increment() 函数(写操作)。

如果在测试中遇到具体的报错(如 Invalid JSON-RPC responseNonce too lowInsufficient funds),可以查看以太坊节点的错误日志(Geth日志)或使用像Tenderly这样的工具调试交易。

抱歉,评论功能暂时关闭!